Ответ
В моих проектах на 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.
Ответ 18+ 🔞
Да ты посмотри, какой у меня тут бардак в голове после этих всех архитектур! Сижу, блядь, думаю, как бы так сделать, чтобы через полгода самому не охуеть от своего же кода. Ну и пришёл к выводу, что надо всё по полочкам, ёпта. Не абы как.
Вот смотри, я в Flutter'е обычно делю всю эту пиздопроебибну на три основных слоя. Как будто бутерброд делаешь: хлеб сверху, хлеб снизу, а посередине — самое важное, колбаса. Только тут не колбаса, а логика, которую потом не стыдно будет тестить.
Первый слой — это лицо проекта, Presentation Layer. Его задача — простая как три копейки: показывать кнопочки, текст и слушать, куда пользователь тыкает. Всё. Больше от него ничего не требуется. Делаешь виджеты максимально тупыми, чтобы они только получали готовые данные и колбэки, а сами нихуя не решали. Чистая картинка.
// Смотри, какой виджет-дурачок. Ему всё в рот положили.
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'),
),
],
);
}
}
Вот и вся его ответственность. Никаких http.get или запросов к базе тут быть не должно, ядрёна вошь! Иначе потом разбираться — волнение ебать.
Второй слой — это святое, Domain Layer. Вот тут живёт вся бизнес-логика, мозги приложения. Самое интересное. Этот слой вообще не должен знать, что такое Flutter, Dio или SQLite. Он чистый, как слеза младенца. Тут твои Use Cases (или интеракторы, кому как удобнее) и интерфейсы репозиториев.
// 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, общага для данных. Тут уже живут все эти штуки, которые реально лезут в сеть, пишут в базу, парсят JSON'ы. Репозитории здесь получают конкретную имплементацию. Слой, где вся грязная работа.
// А вот тут репозиторий уже знает КАК именно достать юзера.
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); // Если интернета нет — берём из кэша
}
}
}
Представляешь, завтра бэкендеры взбесятся и с REST'а переедут на GraphQL? Ты меняешь только UserRemoteDataSource, а вся остальная логика приложения даже не чихнет. Красота.
А как же это всё связать?
Вот тут на сцену выходят наши спасители — Bloc (Cubit) или Provider. Это такие менеджеры-посредники, у которых доверия ебать ноль. Виджет говорит ему: "Эй, нужно загрузить профиль!" Блок говорит Use Case'у: "Слышь, выполни-ка вот это". Use Case стучится в репозиторий, репозиторий мечется между сетью и базой, и в итоге блок получает либо юзера, либо ошибку. Он эту всю историю упаковывает в новое состояние (Loading, Loaded, Error) и кричит виджету: "Эй, дурачок, обновляйся! Вот тебе данные".
Итог получается простой: виджеты — рисуют, домен — думает, данные — носятся как угорелые между сервером и телефоном. И когда через полгода придёт новый чувак в проект и спросит "а где тут запрос на апишку?", ты не будешь полчаса искать её в 3000 строк кода одного виджета, а просто пойдёшь в слой данных. И жить станет проще, ёпта.