Как распределяется ответственность между элементами во Flutter-приложении?

«Как распределяется ответственность между элементами во Flutter-приложении?» — вопрос из категории Архитектура, который задают на 36% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В моих проектах на Flutter я придерживаюсь четкого разделения ответственности, обычно следуя принципам Clean Architecture или ее адаптациям (например, Reso Coder's/Flutter TDD). Это позволяет поддерживать код тестируемым, масштабируемым и понятным для команды.

Слои и их зоны ответственности:

  1. 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'),
          ),
        ],
      );
      }
      }
  2. 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);
      }
      }
  3. 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.