Архитектура Flutter проекта простым языком. Clean Arch (MVVM, DI, Bloc, Inversion of Control)
Для чего нужна архитектура?
В мире программирования наличие общепринятой архитектуры проекта имеет решающее значение для успешного выполнения и поддержания проекта. Архитектура определяет структуру, взаимосвязь между компонентами и их взаимодействия. Четко разработанная архитектура не только упрощает процесс разработки, но и обеспечивает масштабируемость, гибкость и удобство сопровождения кода. В конечном итоге, архитектурное согласие в команде повышает производительность, качество конечного продукта и снижает затраты на разработку и поддержку.
Clean Architecture
Clean Architecture является одной из самых популярных архитектур в мире разработки программного обеспечения, особенно для крупных и средних проектов. Она разработана Робертом Мартином (Uncle Bob) и получила широкое распространение благодаря своей модульности, гибкости и легкости в сопровождении.
Почему же она завоевала такую популярность и симпатию среди Flutter разработчиков и не только? Вот несколько причин, почему эта архитектура столь популярна и эффективна:
Четкое разделение ответственности на слои:
Презентационный слой (Presentation): Отвечает за пользовательский интерфейс и взаимодействие с пользователем. Здесь находятся виджеты Flutter, которые отображают данные и обрабатывают пользовательские действия. Кроме интерфейса в текущем слое находится наш менеджер состояния и view модель.
Слой домена (Domain): Содержит бизнес-логику и правила приложения. Это сердцевина нашей фичи или приложения, которая независима от фреймворков и внешних библиотек.
Слой данных (Data): Отвечает за управление данными, включая работу с базами данных, сетевыми запросами и кешированием.
Позже мы разберем где и какие именно папки, классы и их реализации должны находится.
Какие шаблоны проектирования нужны для чистой архитектуры?
MVVM - Model View View Model - это архитектурный паттерн, который используется для разделения логики представления (UI) и бизнес-логики в приложениях, стоит использовать только в том случае если в нашем виджете , который должен быть максимально “глупым”, существует зависимости от репозитория или же методов. Если присутствует большое количество переменных, переопределенных методов по типу initState так же нужно создать viewModel, в которую мы перенесем всю ненужную "глупому" виджету логику. Ниже нарисованная схема поможет лучше понять смысл этого паттерна.
Inversion of Control (инверсия управления) - это некий абстрактный принцип, набор рекомендаций для написания слабо связанного кода. Суть которого в том, что каждый компонент системы должен быть как можно более изолированным от других, не полагаясь в своей работе на детали конкретной реализации других компонентов.
Dependency Injection (внедрение зависимостей) - это одна из реализаций этого принципа помимо этого есть еще: Service Locator - широко известный анти-шаблон. А чем он занимается? Предоставляет доступ одних объектов к другим объектам.
Принципы ViewModel
В каждом проекте вы неизбежно столкнетесь с использованием ViewModel. Понимание этой концепции часто вызывает затруднения у начинающих разработчиков. Поэтому не будет лишним её разобрать.
Model - Модель представляет данные и бизнес-логику приложения. Она отвечает за управление данными и обеспечение согласованности и целостности приложения. В нашем случае Model будет абстрактным репозиторием из Domain слоя.
View - View отвечает за представление данных пользователю и захват пользовательских взаимодействий. Это пользовательский интерфейс, с которым взаимодействуют пользователи. Сохраняйте виджеты максимально «глупыми», минимизируя логику в компонентах пользовательского интерфейса вынося их в ViewModel.
ViewModel - это посредник между Model и View. Он содержит логику представления, раскрывая данные и команды, к которым View может привязываться. ViewModel разработан для тестирования независимо от UI. Он также часто инкапсулирует состояние View и обрабатывает ввод и взаимодействие пользователя.
Пример ViewModel:
class MapViewModel {
MapViewModel({required this.context,required this.infoLocationRepository,required this.locationRepository});
final BuildContext context;
final LocationRepository locationRepository;
final InfoLocationRepository infoLocationRepository;
late final UserLocationBloc locationBloc = UserLocationBloc(repository: locationRepository);
late final UserInfoLocationBloc userInfoLocationBloc = UserInfoLocationBloc(infoLocationRepository: infoLocationRepository);
late GoogleMapController mapController;
ValueNotifier selectLat = ValueNotifier([double]);
ValueNotifier selectLng = ValueNotifier([double]);
String styleMap = '';
get onMapCreated => _onMapCreated;
get initialize => _initialize;
void _onMapCreated(GoogleMapController controller) {
mapController = controller;
}
void _initialize() {
locationBloc.add(GetStartUserLocation());
DefaultAssetBundle.of(context).loadString('style.json').then((onValue) {
styleMap = onValue;
});
}
}
Теперь наш виджет по настоящему "глупый", в нем нет реализации методов и прочего, теперь это все в нашей view model.
Структура проекта:
Utils
Utils содержит в себе все настройки и коллекции приложения. Тема, цвета нашего приложения. Коллекции с нашими svg/png картинками. Какая-то часть разработчиков называют данную папку theme.
Features
Features папка будет содержать все функции приложения, такие как аутентификация, профиль… А уже каждая фича будет разделяться на слои:
Data Layer
Уровень данных отвечает за взаимодействие с внешними источниками данных, такими как базы данных, сетевые службы или репозитории. Он управляет хранением и извлечением данных.
Repositories - Здесь хранится вся реализация бизнес логики, которая имплементируется от абстрактных классов описанных в Domain.
Models - Модельки наших ответов с сервера.
Service - Тут хранится реализация логики обращения к API или же к БД. В моем случае я использую библиотеку Retrofit, которая не нуждается в обработке и долгой реализации, именно поэтому наш абстрактный класс будет реализован сразу в Data. Как вы видите благодаря многим инструментам мы можем отойти от строгости структурирования чистой архитектуры.
Domain Layer
Domain Layer, также известный как Business Logic или Use Case Layer, содержит основные бизнес-правила и логику приложения. Он представляет собой сердце программной системы, инкапсулируя существенную функциональность, которая не зависит от какой-либо конкретной структуры.
Repositories - Здесь мы объявляем наши абстрактные классы, которые реализованы в data.
Entity - наши сущности, отличаются от моделей тем, что модельки используются для того, чтобы распарсить данные, а сущности для того чтобы использовать их.
Use_case - часто реализуются в виде отдельных классов или методов, которые инкапсулируют бизнес-логику. Это позволяет отделить бизнес-логику от деталей реализации (интерфейсы пользователя, базы данных и т.д.) и сделать код более модульным и тестируемым.
Presentation Layer
Presentation Layer (слой представления) в программировании — это слой архитектуры приложения, который отвечает за отображение данных пользователю и обработку взаимодействий пользователя с приложением. Он играет важную роль в том, как пользователи взаимодействуют с приложением и как данные и команды передаются между пользователем и бизнес-логикой.
Bloc - У вас эта папка может называться по другому , все зависит от вашего стейт-менеджера, у меня это блок.
Page - здесь хранятся наша верстка.
View Model - Здесь находится модель наших экранов
Widget - Виджеты которые дают верстке не читабельный вид и требуют вынесения в отдельные файлы (например из-за своей громоздкости).
Core
Папка core представляет собой фундаментальный модуль, содержащий ключевые компоненты, такие как утилиты, маршруты, сеть, службы, валидаторы и т. д. Часть разработчиков кучу папок суют в core, а кто-то разделяет как и features на слои: domain, data, presentation; как это делаю я и большинство разработчиков. Так легче ориентироваться и находить нужные тебе файлы.
Data:
Storage - Описание работы с нашим хранилищем.
Translation - наша логика перевода.
Domain:
Di - di container в котором мы проинициализируем зависимости приложения (классы которые должны быть single ton).
Repository - здесь хранится вся логика наших шаблонов применяемых к большому количеству реализаций. Например Dio Interceptor - настраивает все запросы (В моем случае добавляет в хедеры токен, который используется во всех запросах) или ответы. (У меня при ответе сервера 401 ошибки выполняется перезаписывание токена и повторное обращение к серверу)
Router - тут находятся наши пути, по которым мы будем перемещаться между экранами.
Use_case - тот же шаблон как и repository но применяемый не для самой реализации, а для обработки существующей реализации. Например шаблон ответа от сервера:
sealed class Result<T> {
bool get isSuccess;
const factory Result.data(T data) = DataResult;
const factory Result.error(List<String> errorList) = ErrorResult;
}
class DataResult<T> implements Result<T> {
final T data;
const DataResult(this.data);
@override
bool get isSuccess => true;
}
class VoidResult<T> implements DataResult<T?> {
@override
bool get isSuccess => true;
@override
final T? data;
const VoidResult([this.data]);
}
class ErrorResult<T> implements Result<T> {
final List<String> errorList;
const ErrorResult(this.errorList);
@override
bool get isSuccess => false;
}
То есть мы обрабатываем уже ответ от существующей реализации.
Presentation:
Page - тут находятся наши базовые скрины, такие как BottomNavigation. Которые управляют навигацией всего приложения.
Widget - тут все, часто переиспользуемые виджеты.
У чистой архитектуры нет строгих принципов. Это лишь базовые папки и подходы, которые были описаны в этой статье. В каждой компании свои подходы, но познакомившись с той структурой, что описал я, не будет никаких проблем разобраться в каком либо уже существующем проекте. Я вам гарантирую, что в core/domain не будут только те папки, сказаны мною выше. Там может находится логика обработки push уведомлений, логика входа с помощью отпечатка пальца и так далее. Каждый проект несет за собой свои особенности и фичи.
Но если вам скажут создать новый функционал, например реализовать передачу местоположения пользователя в фоном режиме, вы уже знаете что это будет описано по пути core/domain/repository и core/data/repository. В core потому это не является какой-то фичей приложения, у нее нет ui, она работает в любом месте, в любых условиях. В domain потому что у нас выполняется логика - достать место положение. Нам придется создать абстрактный класс:
abstract class Location {
Location getLocation();
}
Далее в папке core/data/repository- мы уже выполняем его реализацию
class LocationImpl implements Location {
@override
Location getLocation(){
//code
};
}
Надеюсь, эта статья была для вас полезна, желаю вам удачи 😃