Ранее мы обсудили принцип работы паттерна BLoC. Вы познакомились с потоками данных, однако реализация даже простых задач требовала значительного объёма кода. Для решения этой проблемы существует более простой подход, который мы рассмотрим в этом параграфе.
Встречайте — библиотека bloc
от Феликса Анжелова (англ. Felix Angelov). Это инструмент, который значительно упрощает работу с паттерном BLoC. Она избавляет от рутинных задач, минимизирует риск ошибок в потоках данных и сокращает объём шаблонного кода за счёт готовых абстракций и встроенных механизмов управления состоянием.
Мы уже упоминали вскользь об этой библиотеке и о том, как она повлияла на реализацию паттерна BLoC, в предыдущем параграфе. Теперь самое время познакомиться с библиотекой поближе и научиться её использовать.
Первые шаги
Примечание
Примечание
В этом параграфе используются следующие версии библиотек:
-
bloc: ^9.0.0
-
flutter_bloc: ^9.1.1
-
bloc_concurrency: ^0.3.0
Для начала работы подключим библиотеку к Flutter-проекту:
1flutter pub add bloc
Создаём файл counter_bloc.dart
и импортируем библиотеку:
1import 'package:bloc/bloc.dart';
Класс Bloc<Event, State>
— это сердце библиотеки bloc
. Он принимает события (Event
) и преобразует их в состояния (State
), инкапсулируя всю бизнес-логику вашего приложения. Вы уже знакомы с концепцией событий и состояний, так что давайте сразу перейдём к делу: как это работает на практике.
Создаём CounterBloc
, который будет реализовывать механизм счётчика. В качестве события (Bloc<CounterEvent, ...>
) укажем класс CounterEvent
, а в качестве состояния укажем int
(Bloc<CounterEvent, int>
).
1sealed class CounterEvent {
2 const CounterEvent();
3
4 // Событие для увеличения счётчика
5 const factory CounterEvent.increment() = IncrementCounterEvent;
6
7 // Событие для уменьшения счётчика
8 const factory CounterEvent.decrement() = DecrementCounterEvent;
9}
10
11final class IncrementCounterEvent extends CounterEvent {
12 const IncrementCounterEvent();
13}
14
15final class DecrementCounterEvent extends CounterEvent {
16 const DecrementCounterEvent();
17}
18
19class CounterBloc extends Bloc<CounterEvent, int> {
20 CounterBloc(super.state) {
21 ...
22 }
23 ...
24}
При ручной реализации паттерна BLoC требовалось создавать StreamController
, настраивать преобразование событий через mapEventToStates
, управлять подписками и обрабатывать ошибки вручную.
Библиотека bloc
предлагает простое решение — метод on<Event>
. Он работает как умный диспетчер: принимает события определённого типа и автоматически направляет их в нужный обработчик, избавляя вас от необходимости писать весь этот низкоуровневый код.
Примечание
Примечание
Метод on<Event>
был внедрён в ответ на проблему в Dart SDK, связанную с вложенными асинхронными генераторами.
Феликс Анжелов разработал это решение, заменив использование вложенных асинхронных генераторов на более надёжный подход с методом on<Event>
.
Рассмотрим структуру и применение метода on<Event>
:
-
Тип события. Определяется через параметр типа
<IncrementCounterEvent>
и<DecrementCounterEvent>
. -
Обработка. Реализуется через функцию-обработчик.
1class CounterBloc extends Bloc<CounterEvent, int> {
2 CounterBloc(super.state) {
3 on<IncrementCounterEvent>(_increment);
4 on<DecrementCounterEvent>(_decrement);
5 }
6
7 Future<void> _increment(IncrementCounterEvent event, Emitter<int> emit) async {
8 final newState = state + 1;
9 emit(newState);
10 }
11
12 Future<void> _decrement(DecrementCounterEvent event, Emitter<int> emit) async {
13 final newState = state - 1;
14 emit(newState);
15 }
16}
Класс Emitter
предоставляет нам возможность установить состояние. Можно воспринимать его как метод setState
, который принимает новое состояние. У этого класса есть метод void call(State state)
, поэтому мы можем использовать его как функцию.
Теперь, когда мы создали наш первый Bloc
, давайте рассмотрим несколько ключевых правил, которые обеспечивают его корректную и эффективную работу.
Регистрация событий
Каждое событие должно быть зарегистрировано в Bloc
через соответствующий обработчик. Это обеспечивает контроль над обработкой событий и предотвращает возникновение ошибок.
Принцип работы:
-
Регистрация события через
on<IncrementCounterEvent>
делает его доступным для обработки. -
Отсутствие регистрации при вызове
bloc.add(const IncrementCounterEvent())
приведёт кStateError
(механизм реализован через assert).
1class CounterBloc extends Bloc<CounterEvent, int> {
2 CounterBloc(super.state) {
3 // Регистрация обработчиков событий
4 on<IncrementCounterEvent>(_increment); // ✅
5 on<DecrementCounterEvent>(_decrement); // ✅
6 // Отсутствие регистрации приведёт к ошибке
7 }
8}
Управление потоком событий и bloc_concurrency
При параллельной обработке событий возможно возникновение состояния гонки. Рассмотрим пример:
1class CounterBloc extends Bloc<CounterEvent, int> {
2 CounterBloc(super.state) {
3 on<IncrementCounterEvent>(_increment);
4 ...
5 }
6
7 Future<void> _increment(IncrementCounterEvent event, Emitter<int> emit) async {
8 final newState = state + 1;
9 // Имитация асинхронной операции
10 await Future.delayed(Duration(seconds: 1));
11 emit(newState);
12 }
13
14 ...
15}
При одновременном добавлении двух событий:
1final bloc = CounterBloc(0);
2bloc.add(const CounterEvent.increment());
3bloc.add(const CounterEvent.increment());
Возникает следующая ситуация:
-
Оба обработчика получают
final newState = state + 1;
= 1. -
Оба устанавливают
state = 1
(вместо ожидаемой последовательности 0 -> 1 -> 2).
По умолчанию Bloc
обрабатывает события параллельно, что может привести к некорректному состоянию.
Для решения этой проблемы используется механизм трансформеров, которые предоставляет библиотека bloc_concurrency
.
Добавим библиотеку bloc_concurrency
:
1flutter pub add bloc_concurrency
Импортируем библиотеку:
1import 'package:bloc_concurrency/bloc_concurrency.dart';
Эта библиотека содержит реализации трансформеров для разного способа обработки событий. Устанавливаем нашему обработчику IncrementCounterEvent
трансформер sequential()
:
1class CounterBloc extends Bloc<CounterEvent, int> {
2 CounterBloc(super.state) {
3 on<IncrementCounterEvent>(_increment, transformer: sequential());
4 ...
5 }
6
7 Future<void> _increment(IncrementCounterEvent event, Emitter<int> emit) {
8 final newState = state + 1;
9 await Future.delayed(Duration(seconds: 1));
10 emit(newState);
11 }
12
13 ...
14}
Трансформер sequential
обеспечивает последовательную обработку событий:
- Первое событие выполнится и установит значение
state
равным 1. - Второе событие выполнится после первого и установит значение
state
равным 2.
У каждого обработчика on<Event>
своя очередь обработки. Мы уже определили, что IncrementCounterEvent
обрабатывается в порядке очереди с помощью трансформера sequential
. Но для DecrementCounterEvent
трансформер не был указан, и у него есть собственная очередь. Даже если мы добавим трансформер sequential
для обоих обработчиков, это не создаст общую очередь.
Чтобы события DecrementCounterEvent
и IncrementCounterEvent
имели общую очередь, нужно использовать только один обработчик on<Event>
. Это позволит объединить очереди в одну. Давайте доработаем наш CounterBloc
:
1class CounterBloc extends Bloc<CounterEvent, int> {
2 CounterBloc(super.state) {
3 on<CounterEvent>(
4 (event, emit) => switch (event) {
5 IncrementCounterEvent() => _increment(event, emit),
6 DecrementCounterEvent() => _decrement(event, emit),
7 },
8 transformer: sequential(),
9 );
10 }
11
12 Future<void> _increment(IncrementCounterEvent event, Emitter<int> emit) async {
13 final newState = state + 1;
14 await Future.delayed(const Duration(seconds: 1));
15 emit(newState);
16 }
17
18 Future<void> _decrement(DecrementCounterEvent event, Emitter<int> emit) async {
19 final newState = state - 1;
20 await Future.delayed(const Duration(seconds: 1));
21 emit(newState);
22 }
23}
Мы избавились от регистрации нескольких обработчиков в пользу одного обработчика, который обобщён родительским типом CounterEvent
. Использование switch
также гарантирует, что мы не забудем зарегистрировать и обработать событие.
Теперь все события обрабатываются в порядке очереди. Этот подход рекомендуется использовать в большинстве случаев.
Параллельная обработка событий нужна редко, но необходимость может возникнуть. Например, когда пользователь работает с формой, и нажимает на несколько кнопок одновременно, и каждая кнопка отправляет событие:
1class FilterBloc extends Bloc<FilterEvent, FilterState> {
2 FilterBloc() : super(FilterState.initial()) {
3 on<FilterEvent>(
4 (event, emit) => switch (event) {
5 SetCategoryFilterEvent() => _setCategoryFilter(event, emit),
6 SetPriceFilterEvent() => _setPriceFilter(event, emit),
7 SetRatingFilterEvent() => _setRatingFilter(event, emit),
8 },
9 transformer: concurrent(),
10 );
11 }
12
13 void _setCategoryFilter(SetCategoryFilterEvent event, Emitter<FilterState> emit) {
14 emit(state.copyWith(categoryFilter: event.category));
15 }
16
17 void _setPriceFilter(SetPriceFilterEvent event, Emitter<FilterState> emit) {
18 emit(state.copyWith(priceFilter: event.priceRange));
19 }
20
21 void _setRatingFilter(SetRatingFilterEvent event, Emitter<FilterState> emit) {
22 emit(state.copyWith(ratingFilter: event.minRating));
23 }
24}
Параллельная обработка позволяет применять разные фильтры одновременно (категория, цена, рейтинг), каждый фильтр изменяется независимо, не блокируя другие, состояние безопасно обновляется, так как все операции синхронные.
Если стратегия у обработчика не задана, то в таком случае будет использоваться стратегия по умолчанию. Теперь давайте рассмотрим, как установить стратегию по умолчанию для всех обработчиков.
1// Установка последовательной обработки по умолчанию
2Bloc.transformer = sequential();
Для возврата к параллельной обработке:
1// Установка параллельной обработки по умолчанию
2Bloc.transformer = concurrent();
Доступные стратегии
Библиотека bloc_concurrency
предоставляет различные стратегии для управления событиями. Главная роль этой библиотеки — дать разработчику шаблонный набор готовых стратегий. Конечно, вы можете написать свои собственные стратегии, если вдруг в этом появится необходимость.
Concurrent. Позволяет обрабатывать все события параллельно. Например, когда пользователь быстро нажимает на несколько кнопок в интерфейсе, все нажатия будут обработаны одновременно, не дожидаясь завершения предыдущих.
Sequential. Обработка событий происходит поочерёдно, гарантируя, что ни одно событие не начнёт обработку, пока предыдущее не завершится. Как в очереди в магазине — каждый следующий покупатель ждёт, пока обслужат предыдущего.
Restartable. Отменяет выполнение текущего события при поступлении нового, но не прерывает уже запущенный код. Однако дальнейшие вызовы emit
игнорируются — состояние больше не обновляется. Представьте поисковую строку — когда пользователь быстро печатает, нет смысла выполнять поиск для каждой буквы, достаточно обработать только последний запрос.
Чтобы корректно обработать отмену, можно использовать emit.isDone
— это свойство, позволяющее проверить текущий статус обработки события. Если true
, логику можно прервать для оптимизации производительности.
Droppable. Обеспечивает обработку только первого события с игнорированием последующих до завершения текущей обработки. Как кнопка отправки формы — повторные нажатия игнорируются, пока не завершится первая отправка.
У этих стратегий нет преимуществ или недостатков. Их цель — помочь вам достичь желаемого результата. Если она приводит к ошибкам, подумайте о выборе другой стратегии или пересмотрите её использование.
Cubit
: Упрощённая альтернатива Bloc
Cubit
— это легковесная реализация паттерна BLoC, который управляет состоянием и позволяет обновлять его с помощью методов. Его основной отличительной чертой является использования методов вместо событий (Event).
1class CounterCubit extends Cubit<int> {
2 CounterCubit(super.initialState);
3
4 Future<void> increment() async {
5 final newState = state + 1;
6 await Future.delayed(const Duration(seconds: 1));
7 emit(newState);
8 }
9
10 Future<void> decrement() async {
11 final newState = state - 1;
12 await Future.delayed(const Duration(seconds: 1));
13 emit(newState);
14 }
15}
Cubit
не имеет встроенного механизма контроля очереди событий, что может привести к проблемам с консистентностью состояния. Рекомендуется использовать его в случаях, когда порядок обработки событий не критичен для логики приложения.
Мониторинг и отладка с BlocObserver
BlocObserver
— абстрактный класс, представляющий собой инструмент мониторинга состояний и событий в приложении. При наследовании и переопределении его методов вы получаете возможность отслеживать изменения состояния, обработку событий и возникающие ошибки во всех Bloc
- и Cubit
-компонентах приложения. Это поможет вам с отладкой, аналитикой и мониторингом приложения.
Методы мониторинга
Метод onCreate
-
Назначение — отслеживание инициализации экземпляров
Bloc
/Cubit
. -
Применение — логирование инициализации зависимостей.
1void onCreate(BlocBase<Object?> bloc) {
2 super.onCreate(bloc);
3 print('Создан новый Bloc: ${bloc.runtimeType}');
4}
Метод onEvent
-
Назначение — мониторинг входящих событий.
-
Применение — анализ потока событий.
1void onEvent(Bloc<Object?, Object?> bloc, Object? event) {
2 super.onEvent(bloc, event);
3 print('Событие в ${bloc.runtimeType}: $event');
4}
Метод onChange
-
Назначение — мониторинг изменений состояния.
-
Применение — анализ изменений состояния, сбор аналитических данных.
1void onChange(BlocBase<Object?> bloc, Change<Object?> change) {
2 super.onChange(bloc, change);
3 print(
4 'Изменение состояния ${bloc.runtimeType}: '
5 '${change.currentState} → ${change.nextState}',
6 );
7}
Метод onTransition
-
Назначение — мониторинг переходов состояния в
Bloc
. -
Применение — анализ цепочек преобразования «Событие → Состояние».
1void onTransition(
2 Bloc<Object?, Object?> bloc,
3 Transition<Object?, Object?> transition,
4) {
5 super.onTransition(bloc, transition);
6 final event = transition.event;
7 final currentState = transition.currentState;
8 final nextState = transition.nextState;
9 print(
10 'Переход ${bloc.runtimeType}: $event '
11 'из $currentState в $nextState',
12 );
13}
Метод onError
-
Назначение — обработка ошибок в
Bloc
/Cubit
. -
Применение — регистрация ошибок, анализ исключений.
1void onError(BlocBase<Object?> bloc, Object error, StackTrace stackTrace) {
2 super.onError(bloc, error, stackTrace);
3 print('Ошибка в ${bloc.runtimeType}: $error \nSk: $stackTrace');
4}
Метод onClose
-
Назначение — мониторинг завершения работы
Bloc
/Cubit
. -
Применение — контроль освобождения ресурсов.
1void onClose(BlocBase<Object?> bloc) {
2 super.onClose(bloc);
3 print('Bloc ${bloc.runtimeType} закрыт');
4}
Подключение BlocObserver
Реализация класса-наблюдателя:
1class AppBlocObserver extends BlocObserver {
2 const AppBlocObserver();
3 // Реализация необходимых методов
4}
Регистрация наблюдателя:
1void main() {
2 Bloc.observer = const AppBlocObserver();
3 runApp(const MyApp());
4}
Пример интеграции с системой аналитики FirebaseCrashlytics
:
1void onError(BlocBase<Object?> bloc, Object error, StackTrace stackTrace) {
2 FirebaseCrashlytics.instance.recordError(error, stackTrace);
3 super.onError(bloc, error, stackTrace);
4}
Примечание
Примечание
Подробнее о Firebase можно узнать в параграфе о Firebase.
Обработка ошибок в Bloc
реализуется через метод addError
:
1class CounterBloc extends Bloc<CounterEvent, CounterState> {
2 ...
3
4 Future<void> _increment(IncrementCounterEvent event, Emitter emit) async {
5 try {
6 final newState = state + 1;
7 await Future.delayed(const Duration(seconds: 1));
8 emit(newState);
9 } on Object catch (error, sk) {
10 // Отправляем нашу ошибку в observer
11 addError(error, sk);
12 }
13 }
14}
Если обработчик событий в Bloc
не содержит конструкции try/catch
для обработки ошибок, Bloc
всё равно обеспечивает контроль над ошибкой. При возникновении ошибки Bloc
автоматически перехватывает исключение и перенаправляет его в BlocObserver
через метод onError
. Параллельно с этим необработанное исключение (англ. unhandled exception) попадаёт в текущую зону выполнения (Zone
), где может быть обработано глобальными обработчиками ошибок.
Таким образом, BlocObserver
обеспечивает функционал для отладки, мониторинга и анализа работы всех Bloc
/Cubit
в приложении.
Интеграция с Flutter: flutter_bloc
flutter_bloc
— это библиотека с набором виджетов, которые позволяют упростить взаимодействие нашего Bloc
с пользовательским интерфейсом Flutter. Рассмотрим основные компоненты библиотеки.
BlocProvider
: внедрение зависимостей
BlocProvider
представляет собой специализированную версию Provider
, оптимизированную для работы с Bloc
и Cubit
. Он обеспечивает управление жизненным циклом объектов в дереве виджетов.
Примечание
Примечание
Для понимания основ внедрения зависимостей рекомендуется ознакомиться с параграфом о Provider.
Функциональность BlocProvider:
Создание и внедрение экземпляров Bloc
/Cubit
в дерево виджетов:
1BlocProvider(
2 create: (context) => CounterBloc(),
3 child: const MyApp(),
4);
Доступ к экземпляру Bloc
из дочерних виджетов:
1final bloc = context.read<CounterBloc>();
2bloc.add(const IncrementEvent());
Инстанс, который был создан через BlocProvider
, автоматически закроется (вызовется метод close
), когда виджет BlocProvider
удалится из дерева виджетов.
Но иногда, возможно даже чаще, чем вы можете себе представить, наши инстансы будут создаваться в другом месте, и тогда необходимо использовать другой способ внедрения нашего Bloc
в дерево. Для этого используется BlocProvider.value
.
Альтернативный метод внедрения существующих экземпляров:
1final bloc = CounterBloc();
2
3BlocProvider.value(
4 value: bloc,
5 child: const MyApp(),
6);
При использовании BlocProvider.value
необходимо обеспечить освобождение ресурсов:
1void dispose() {
2 bloc.close();
3 super.dispose();
4}
Для управления множественными зависимостями используется MultiBlocProvider
:
1MultiBlocProvider(
2 providers: [
3 BlocProvider(create: (context) => CounterBloc(), lazy: false),
4 BlocProvider.value(value: anotherBloc),
5 ],
6 child: const MyApp(),
7);
Виджет BlocProvider
имеет конфигурационное поле lazy
, которое по умолчанию установлено в значение true
. Данный параметр определяет стратегию инициализации: при значении true
создание экземпляра Bloc
откладывается до момента первого обращения к нему.
Виджеты для работы с UI
Виджеты BlocListener
, BlocBuilder
, BlocSelector
и BlocConsumer
обеспечивают автоматический доступ к Bloc
через дерево виджетов:
1BlocProvider(
2 create: (context) => CounterBloc(),
3 child: BlocListener<CounterBloc, CounterState>(
4 listener: ...,
5 child: ...,
6 ),
7)
Возможно явно указать экземпляр Bloc
:
1final bloc = CounterBloc();
2
3BlocListener<CounterBloc, CounterState>(
4 bloc: bloc,
5 listener: (context, state) { ... },
6 child: ...,
7)
Теперь давайте рассмотрим каждый из этих виджетов подробно, чтобы понять их особенности и возможности использования.
BlocListener
: обработка состояний
BlocListener
предназначен для выполнения действий в ответ на изменения состояния без перестроения интерфейса.
1BlocListener<TaskBloc, TaskState>(
2 listener: (context, state) {
3 if (state is TaskSuccess) {
4 ScaffoldMessenger.of(context).showSnackBar(
5 SnackBar(content: Text('Message: ${state.message}')),
6 );
7 }
8 },
9 child: const TaskWidget(),
10);
Пример обработки исключений:
1if (state is TaskFailure) {
2 showDialog(...);
3}
Фильтрация состояний с помощью параметра listenWhen
:
1BlocListener<TaskBloc, TaskState>(
2 listenWhen: (prevState, newState) {
3 return newState is SpecificState;
4 },
5 listener: (context, state) {
6 // Обработка состояния
7 },
8 child: const TaskWidget(),
9);
BlocBuilder
: обновление интерфейса
BlocBuilder
обеспечивает перестроение виджета при изменении состояния:
1BlocBuilder<TaskBloc, TaskState>(
2 builder: (context, state) => TaskWidget(isActive: state.isActive),
3);
BlocBuilder
содержит под капотом BlocListener
, который в коллбэке listener
вызывает метод setState
, тем самым наш интерфейс перестраивается при изменении состояния. BlocBuilder
обладает всеми теми же возможностями, какими обладал BlocListener
. Если мы хотим, чтобы наш виджет перестраивался в определённых состояниях, можем указать параметр buildWhen
:
1BlocBuilder<TaskBloc, TaskState>(
2 buildWhen: (prevState, newState) => prevState.isActive != newState.isActive,
3 builder: (context, state) => TaskWidget(isActive: state.isActive),
4);
BlocConsumer
: комплексный подход
BlocConsumer
объединяет функциональность BlocListener
и BlocBuilder
:
1BlocConsumer<TaskBloc, TaskState>(
2 listenWhen: (prevState, newState) => newState is TaskSuccess,
3 buildWhen: (prevState, newState) => prevState.isActive != newState.isActive,
4 listener: (context, state) {
5 if (state is TaskSuccess) {
6 ScaffoldMessenger.of(context).showSnackBar(
7 SnackBar(content: Text('Message: ${state.message}')),
8 );
9 }
10 },
11 builder: (context, state) => TaskWidget(isActive: state.isActive),
12);
BlocSelector
: оптимизация обновлений
BlocSelector
оптимизирует перестроение интерфейса, отслеживая определённые параметры состояния:
1BlocSelector<TaskBloc, TaskState, bool>(
2 selector: (state) => state.isActive,
3 builder: (context, isActive) => TaskWidget(isActive: isActive),
4);
В завершение давайте подытожим ключевые моменты, о которых говорили:
-
Библиотека
bloc
— инструмент для управления состоянием, основанный на паттерне BLoC, который помогает структурировать логику приложения и сделать код более поддерживаемым. -
Библиотека
flutter_bloc
содержит такие компоненты, какBlocListener
,BlocBuilder
,BlocConsumer
иBlocSelector
, которые позволяют связывать бизнес-логику с пользовательским интерфейсом. -
Библиотека
bloc_concurrency
предоставляет трансформеры для управления потоком событий.
Если вы хотите углубить свои знания, обязательно загляните в официальную документациюBloc — там вы найдёте множество полезных материалов и практических примеров.
А в следующем параграфе мы познакомимся с другим паттерном для управления состояния — Redux.