Что такое чистая архитектура (Clean Architecture) в контексте Flutter-приложения?

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

Ответ

Чистая архитектура (Clean Architecture) — это подход к проектированию, который разделяет код на слои с четкими правилами зависимостей, направленными внутрь, к домену. Главная цель — создать приложение, которое:

  • Не зависит от фреймворков (Flutter, базы данных, внешние библиотеки).
  • Легко тестируется (бизнес-правила можно тестировать без UI, базы данных или сети).
  • Просто в поддержке и развитии благодаря низкой связанности компонентов.

Типичные слои в Flutter-проекте:

  1. Domain (Домен): Ядро приложения. Содержит бизнес-сущности (Entities) и сценарии использования (Use Cases / Interactors). Не зависит ни от чего внешнего.
  2. Data (Данные): Реализует контракты, определенные в домене (репозитории). Работает с источниками данных (API, локальная БД). Зависит только от Domain-слоя.
  3. Presentation (Представление): Слой UI (виджеты) и управления состоянием (BLoC, Cubit, Provider). Зависит от Domain-слоя (Use Cases) и иногда от Data (через Dependency Injection).

Пример структуры для функции "получить пользователя":

1. Domain Layer (lib/domain/):

// Сущность (Entity)
class User {
  final String id;
  final String name;
  final String email;

  const User({required this.id, required this.name, required this.email});
}

// Абстракция репозитория (контракт)
abstract class UserRepository {
  Future<User> getUserById(String id);
}

// Сценарий использования (Use Case)
class GetUserUseCase {
  final UserRepository repository;

  GetUserUseCase(this.repository);

  Future<User> execute(String userId) async {
    return await repository.getUserById(userId);
  }
}

2. Data Layer (lib/data/):

// Модель данных для API (DTO - Data Transfer Object)
class UserDto {
  final String id;
  final String full_name; // Может отличаться от имени в Entity
  final String email;

  UserDto({required this.id, required this.full_name, required this.email});

  // Конвертация из DTO в Entity
  User toEntity() {
    return User(id: id, name: full_name, email: email);
  }
}

// Конкретная реализация репозитория
class UserRepositoryImpl implements UserRepository {
  final UserApiService apiService;

  UserRepositoryImpl(this.apiService);

  @override
  Future<User> getUserById(String id) async {
    final UserDto dto = await apiService.fetchUser(id);
    return dto.toEntity(); // Преобразуем данные API в доменную сущность
  }
}

3. Presentation Layer (lib/presentation/):

// BLoC для управления состоянием экрана
class UserBloc extends Bloc<UserEvent, UserState> {
  final GetUserUseCase getUserUseCase; // Зависимость от Use Case

  UserBloc(this.getUserUseCase) : super(UserInitial()) {
    on<FetchUserEvent>((event, emit) async {
      emit(UserLoading());
      try {
        final user = await getUserUseCase.execute(event.userId);
        emit(UserLoaded(user));
      } catch (e) {
        emit(UserError(e.toString()));
      }
    });
  }
}

// Виджет, использующий BLoC
class UserScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<UserBloc, UserState>(
      builder: (context, state) {
        if (state is UserLoaded) {
          return Text('Пользователь: ${state.user.name}');
        }
        // ... обработка других состояний (загрузка, ошибка)
      },
    );
  }
}

Такой подход требует больше кода на старте, но окупается на больших и долгоживущих проектах за счет предсказуемости и простоты внесения изменений.

Видео-ответы