Как добиться низкой связности (low coupling) между компонентами во Flutter-приложении?

Ответ

Низкая связность — это ключевой принцип, над которым я работаю в архитектуре Flutter-приложений. Достигается это через четкое разделение ответственности и использование правильных паттернов.

Основные практики, которые я применяю:

  1. Разделение слоев (Layered Architecture):

    • Presentation Layer (UI): Только виджеты. Их задача — отображать состояние и передавать действия пользователя.
    • Business Logic Layer: BLoC/Cubit, ChangeNotifier, ViewModel. Содержат всю логику, обрабатывают события UI, взаимодействуют с репозиториями.
    • Data Layer: Репозитории, Data Sources (Local, Remote). Отвечают за данные, скрывая детали реализации (база данных, API).
  2. Зависимость от абстракций (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
      }
    }
  3. Внедрение зависимостей (Dependency Injection): Использую get_it или riverpod для управления зависимостями. Это позволяет легко подменять реализации (например, на моки для тестов).

    // Регистрация зависимостей
    void setupLocator() {
      getIt.registerLazySingleton<AuthRepository>(() => FirebaseAuthRepository());
      getIt.registerFactory(() => AuthBloc(getIt<AuthRepository>()));
    }
    // Использование в виджете
    final authBloc = getIt<AuthBloc>();
  4. Чистые виджеты (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+ 🔞

А, ну вот, зашёл разговор про архитектуру, ебать мои старые костыли! Это ж святое, чувак. Слушай, тут всё просто, как три копейки, но если накосячить — потом будешь плакать кровавыми слезами, пока переписываешь пол-приложения из-за одной хуйни.

Низкая связность — это, блядь, вообще основа основ. Суть в том, чтобы твой код был не как единый ком грязи, где всё склеено друг с другом, а как конструктор. Одна деталь сломалась — ты её выкинул и новую воткнул, а не весь проект в окно отправляешь.

Как я этого добиваюсь, ёпта:

  1. Слои, как в торте, только полезные.

    • UI-слой (Виджеты): Их дело — красиво рисовать и тыкать в кнопки. Больше них нихуя не волнует. Получил данные — отобразил. Пользователь ткнул — отправил событие куда надо. И всё.
    • Слой логики (BLoC/Cubit/ViewModel): Вот тут уже мозги. Этот слой получает события от UI, думает, хуярит бизнес-правила, лезет за данными в репозиторий и выдает готовое состояние обратно на отрисовку.
    • Слой данных (Репозитории, API, БД): Его задача — достать или сохранить данные. Неважно как: с сервера, из локальной базы или из файла под подушкой. UI и логика об этом даже не догадываются.
  2. Зависимость от интерфейсов, а не от конкретной хуйни. Это, блядь, самый важный пункт, чтобы не оказаться в ситуации «хуй с горы». Твой 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
      }
    }
  3. Внедрение зависимостей — чтобы не таскать всё руками. Ты же не будешь сам по всему приложению таскать экземпляры классов, как мартышлюшка? Используй get_it или riverpod. Зарегистрировал зависимости в одном месте — и потом просто доставай, где надо. Особенно кайфуешь, когда пишешь тесты и подсовываешь моки вместо реальных API.

    // Настраиваем наш "ящик с инструментами" один раз
    void setupLocator() {
      getIt.registerLazySingleton<AuthRepository>(() => FirebaseAuthRepository());
      getIt.registerFactory(() => AuthBloc(getIt<AuthRepository>())); // BLoC сам получит нужный репозиторий
    }
    
    // В виджете просто берём и используем. Красота!
    final authBloc = getIt<AuthBloc>();
  4. Тупые виджеты — залог счастья. Делай виджеты максимально тупыми и переиспользуемыми. Они должны получать всё готовенькое: и данные для отображения, и коллбеки для нажатий. Они не должны ни о чём догадываться.

    // Идеальный тупой виджет. Его можно запихнуть куда угодно.
    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), // Просто отображает переданный текст
            ),
          ),
        );
      }
    }

И что в итоге, ёклмн? А в итоге получается приложение, где каждый модуль живёт своей жизнью. Тестировать — одно удовольствие, потому что всё изолировано. Менять одну часть, не ломая другую — легко. Переиспользовать виджеты и логику в других проектах — вообще не вопрос. В общем, не еби мозг себе и другим, делай по уму.