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

«Как добиться низкой связности (low coupling) между компонентами во Flutter-приложении?» — вопрос из категории Архитектура, который задают на 29% собеседований 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),
            ),
          ),
        );
      }
    }

Результат: Компоненты становятся независимыми, их легко тестировать изолированно, рефакторить и повторно использовать в разных частях приложения или даже в разных проектах.