Добавить объявление

Flutter: выносим бизнес-логику из BLoC в use-cases

При разработке Flutter-приложений паттерн BLoC часто становится всемогущим объектом, впитывающим всю бизнес-логику. Внутри хендлеров оказываются и запросы к сервисам, и валидация и эмиттеры состояния. Проект разрастается, файлы раздуваются, тестирование становится невозможным. Сегодня обсудим как можно вынести бизнес-логику в отдельные классы - use-cases.

Как это работает:


Вместо того чтобы вызывать сервисы прямо внутри хендлеров BLoC, вся логика конкретного сценария (например загрузки товаров) выносится в отдельный класс. Use-case зависит от абстракций репозиториев и сервисов, но ничего не знает про UI. Он вызывает нужные зависимости, обрабатывает ошибки, выполняет side-эффекты и только после этого отправляет событие в BLoC. Сам BLoC превращается в тонкую прослойку: принимает события, обновляет состояние и больше ничего не делает.

Как это работает на практике:


Огромный BLoC-файл, отвечающий за получение данных, кэширование, фильтрацию и обновление UI, превращается в узкое место проекта. Конструктор забит зависимостями, тестирование почти невозможно. После рефакторинга BLoC сокращается до 20-30 строк, не зависит от сервисов и становится просто набором функций для обновления состояния. Все пользовательские сценарии выносятся в отдельные use-cases.

В результате время разработки нового функционала сокращается, количество багов снижается, тесты становятся надежнее.

Пример кода:


На примере ниже показано, как выглядит файл BLoC целиком:

// items_bloc.dart

class ItemsBloc extends Bloc {
ItemsBloc() : super(Initial()) {
on(_onSetLoading);
on(_onSetLoaded);
on(_onSetError);
}

Future _onSetLoading(SetLoading event, Emitter emit) async {
emit(Loading());
}

Future _onSetLoaded(SetLoaded event, Emitter emit) async {
emit(Loaded(items: event.items));
}

Future _onSetError(SetError event, Emitter emit) async {
emit(Error(error: event.error));
}
}

А также реализация use-case:

// fetch_items_use_case.dart

class FetchItemsUseCase {
final ItemsBloc bloc;
final FetchItemsService service;

FetchItemsUseCase({required this.bloc, required this.service});

Future call() async {
bloc.add(SetLoading());
try {
final items = await service.fetchItems();
bloc.add(SetLoaded(items: items));
} catch (error) {
bloc.add(SetError(error: error.toString()));
}
}
}

Почему это работает:


Use-case оркестрирует несколько сервисов и репозиториев, объединяет данные, обрабатывает ошибки, логирует и только после этого отправляет событие в BLoC. BLoC остается только стейт-менеджером и не выполняет ничего, кроме преобразования событий в состояние. Такой подход соответствует принципам чистой архитектуры: доменный слой не зависит от реализации, что повышает гибкость и тестируемость.

Компромисс:


В этой схеме use-case вынужден знать о BLoC (вызывать bloc.add), что формально нарушает идеальную зависимость доменного слоя от презентационного. Но взамен UI освобождается от вызовов bloc.add, а код становится проще и нагляднее. Компромисс оправдан.

Вывод:


Разделение бизнес-логики и UI - необходимость для масштабируемых приложений. Use-cases помогают структурировать код, упрощают тестирование и делают BLoC максимально простым. Этот подход не привязан к BLoC и может использоваться с любым другим стейт-менеджером.
21.04.2026 83 343