Ответ
Низкая связность — это ключевой принцип, над которым я работаю в архитектуре Flutter-приложений. Достигается это через четкое разделение ответственности и использование правильных паттернов.
Основные практики, которые я применяю:
-
Разделение слоев (Layered Architecture):
- Presentation Layer (UI): Только виджеты. Их задача — отображать состояние и передавать действия пользователя.
- Business Logic Layer: BLoC/Cubit, ChangeNotifier, ViewModel. Содержат всю логику, обрабатывают события UI, взаимодействуют с репозиториями.
- Data Layer: Репозитории, Data Sources (Local, Remote). Отвечают за данные, скрывая детали реализации (база данных, API).
-
Зависимость от абстракций (Dependency Inversion): Классы высокого уровня (например, BLoC) не должны зависеть от классов низкого уровня (например, конкретного
ApiClient). Они зависят от интерфейсов (abstract classes).// Абстракция (интерфейс) в domain-слое abstract class AuthRepository { Future<User> signIn(String email, String password); } // BLoC зависит только от абстракции class AuthBloc extends Bloc<AuthEvent, AuthState> { final AuthRepository repository; // Зависимость через интерфейс AuthBloc(this.repository) : super(AuthInitial()) { ... } } // Конкретная реализация в data-слое class FirebaseAuthRepository implements AuthRepository { final FirebaseAuth _firebaseAuth; FirebaseAuthRepository(this._firebaseAuth); @override Future<User> signIn(String email, String password) { // Реализация через Firebase } } -
Внедрение зависимостей (Dependency Injection): Использую
get_itилиriverpodдля управления зависимостями. Это позволяет легко подменять реализации (например, на моки для тестов).// Регистрация зависимостей void setupLocator() { getIt.registerLazySingleton<AuthRepository>(() => FirebaseAuthRepository()); getIt.registerFactory(() => AuthBloc(getIt<AuthRepository>())); } // Использование в виджете final authBloc = getIt<AuthBloc>(); -
Чистые виджеты (Dumb/Presentational Widgets): Стараюсь создавать максимально переиспользуемые виджеты, которые получают все необходимые данные и коллбеки через параметры конструктора.
// Чистый, слабо связанный виджет class UserProfileCard extends StatelessWidget { final String userName; final String? avatarUrl; final VoidCallback onTap; // Виджет ничего не знает о BLoC, Provider или бизнес-логике. const UserProfileCard({ required this.userName, this.avatarUrl, required this.onTap, }); @override Widget build(BuildContext context) { return Card( child: InkWell( onTap: onTap, child: ListTile( leading: CircleAvatar(backgroundImage: NetworkImage(avatarUrl ?? '')), title: Text(userName), ), ), ); } }
Результат: Компоненты становятся независимыми, их легко тестировать изолированно, рефакторить и повторно использовать в разных частях приложения или даже в разных проектах.
Ответ 18+ 🔞
А, ну вот, зашёл разговор про архитектуру, ебать мои старые костыли! Это ж святое, чувак. Слушай, тут всё просто, как три копейки, но если накосячить — потом будешь плакать кровавыми слезами, пока переписываешь пол-приложения из-за одной хуйни.
Низкая связность — это, блядь, вообще основа основ. Суть в том, чтобы твой код был не как единый ком грязи, где всё склеено друг с другом, а как конструктор. Одна деталь сломалась — ты её выкинул и новую воткнул, а не весь проект в окно отправляешь.
Как я этого добиваюсь, ёпта:
-
Слои, как в торте, только полезные.
- UI-слой (Виджеты): Их дело — красиво рисовать и тыкать в кнопки. Больше них нихуя не волнует. Получил данные — отобразил. Пользователь ткнул — отправил событие куда надо. И всё.
- Слой логики (BLoC/Cubit/ViewModel): Вот тут уже мозги. Этот слой получает события от UI, думает, хуярит бизнес-правила, лезет за данными в репозиторий и выдает готовое состояние обратно на отрисовку.
- Слой данных (Репозитории, API, БД): Его задача — достать или сохранить данные. Неважно как: с сервера, из локальной базы или из файла под подушкой. UI и логика об этом даже не догадываются.
-
Зависимость от интерфейсов, а не от конкретной хуйни. Это, блядь, самый важный пункт, чтобы не оказаться в ситуации «хуй с горы». Твой BLoC не должен знать, что там внутри
FirebaseAuthилиRestApiClient. Он должен знать только абстракцию — что есть некийAuthRepository, у которого есть методsignIn. А как он это делает — похуй.// Вот эта штука в domain-слое — наша священная абстракция. Контракт. abstract class AuthRepository { Future<User> signIn(String email, String password); } // Бизнес-логика (BLoC) цепляется только за контракт. Ему похуй на реализацию. class AuthBloc extends Bloc<AuthEvent, AuthState> { final AuthRepository repository; // Смотри-ка, интерфейс! AuthBloc(this.repository) : super(AuthInitial()) { ... } } // А вот в data-слое мы этот контракт выполняем. Хоть через Firebase, хоть через мок. class FirebaseAuthRepository implements AuthRepository { final FirebaseAuth _firebaseAuth; FirebaseAuthRepository(this._firebaseAuth); @override Future<User> signIn(String email, String password) { // А вот тут уже конкретная реализация с Firebase } } -
Внедрение зависимостей — чтобы не таскать всё руками. Ты же не будешь сам по всему приложению таскать экземпляры классов, как мартышлюшка? Используй
get_itилиriverpod. Зарегистрировал зависимости в одном месте — и потом просто доставай, где надо. Особенно кайфуешь, когда пишешь тесты и подсовываешь моки вместо реальных API.// Настраиваем наш "ящик с инструментами" один раз void setupLocator() { getIt.registerLazySingleton<AuthRepository>(() => FirebaseAuthRepository()); getIt.registerFactory(() => AuthBloc(getIt<AuthRepository>())); // BLoC сам получит нужный репозиторий } // В виджете просто берём и используем. Красота! final authBloc = getIt<AuthBloc>(); -
Тупые виджеты — залог счастья. Делай виджеты максимально тупыми и переиспользуемыми. Они должны получать всё готовенькое: и данные для отображения, и коллбеки для нажатий. Они не должны ни о чём догадываться.
// Идеальный тупой виджет. Его можно запихнуть куда угодно. class UserProfileCard extends StatelessWidget { final String userName; final String? avatarUrl; final VoidCallback onTap; // Коллбек пришёл сверху. Виджет не знает, что там будет. // Никаких Provider.of, никаких BlocConsumer внутри! Чистота. const UserProfileCard({ required this.userName, this.avatarUrl, required this.onTap, }); @override Widget build(BuildContext context) { return Card( child: InkWell( onTap: onTap, // Просто вызывает переданный коллбек child: ListTile( leading: CircleAvatar(backgroundImage: NetworkImage(avatarUrl ?? '')), title: Text(userName), // Просто отображает переданный текст ), ), ); } }
И что в итоге, ёклмн? А в итоге получается приложение, где каждый модуль живёт своей жизнью. Тестировать — одно удовольствие, потому что всё изолировано. Менять одну часть, не ломая другую — легко. Переиспользовать виджеты и логику в других проектах — вообще не вопрос. В общем, не еби мозг себе и другим, делай по уму.