Ответ
В моих проектах на Flutter я придерживаюсь четкого разделения ответственности, обычно следуя принципам Clean Architecture или ее адаптациям (например, Reso Coder's/Flutter TDD). Это позволяет поддерживать код тестируемым, масштабируемым и понятным для команды.
Слои и их зоны ответственности:
-
Presentation Layer (Слой представления):
- Ответственность: Отображение UI и обработка пользовательского ввода.
- Компоненты: Виджеты (
StatelessWidget,StatefulWidget),State, контроллеры (PageController,AnimationController). -
Правило: Максимально "тупые" виджеты. Они получают данные и колбэки через параметры.
// Пример "тупого" виджета class UserProfileView extends StatelessWidget { final User user; final VoidCallback onEditPressed; final bool isLoading; const UserProfileView({ required this.user, required this.onEditPressed, this.isLoading = false, }); @override Widget build(BuildContext context) { return Column( children: [ CircleAvatar(url: user.avatarUrl), Text(user.name), if (isLoading) CircularProgressIndicator(), ElevatedButton( onPressed: onEditPressed, child: Text('Edit'), ), ], ); } }
-
Domain Layer (Доменный слой):
- Ответственность: Содержит бизнес-логику и правила приложения. Не зависит от Flutter и внешних библиотек.
-
Компоненты: Entities (бизнес-объекты, например,
User,Product), Use Cases (интеракторы), Repository Interfaces.// Use Case (интерактор) — чистая бизнес-логика class GetUserProfileUseCase { final UserRepository repository; GetUserProfileUseCase(this.repository); Future<Either<Failure, User>> execute(String userId) async { return await repository.getUser(userId); } }
-
Data Layer (Слой данных):
- Ответственность: Работа с данными: получение из сети, кэширование в БД, преобразование моделей.
-
Компоненты: Репозитории (имплементации), Data Sources (API, Local DB), Data Models (DTOs), Mappers.
// Репозиторий (имплементация) соединяет домен с источниками данных class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource remoteDataSource; final UserLocalDataSource localDataSource; @override Future<User> getUser(String id) async { try { final user = await remoteDataSource.fetchUser(id); await localDataSource.cacheUser(user); return user; } on SocketException { return localDataSource.getUser(id); // Fallback на кэш } } }
Связь слоев и управление состоянием: Для связи Presentation и Domain/Data слоев я использую паттерны управления состоянием, такие как Bloc (Cubit) или Provider (ChangeNotifier). Они выступают в роли "посредника":
- Принимают события от UI (например,
UserProfileLoadRequested). - Вызывают Use Cases из доменного слоя.
- Преобразуют результат (
UserилиFailure) в новое состояние (UserProfileLoading,UserProfileLoaded,UserProfileError). - Уведомляют UI об изменении состояния.
Итог: Виджеты отвечают только за UI, бизнес-логика изолирована в доменном слое, а работа с данными инкапсулирована в слое данных. Это позволяет, например, заменить GraphQL на REST API, изменив только Data Layer, или протестировать бизнес-логику без запуска Flutter.