Если вам сложно понять принцип действия библиотеки flutter_bloc
или вы сталкиваетесь с затруднениями при ее реализации, не нужно отчаиваться — сейчас мы пошагово и подробно рассмотрим весь процесс.
Flutter Bloc предлагает простой подход для управления различными состояниями в приложении. Как только вы во всем разберетесь, то сможете легко работать с этой библиотекой и масштабировать приложения любого размера.
Зачем нужна Flutter Bloc
Bloc
позволяет легко отделять презентационный слой от бизнес-логики, делая код быстрым, простым для тестирования и переиспользуемым (официальная документацияBloc
).
Flutter Blocs упрощает процесс управления состояниями в приложении. Она предоставляет простые API, которые абстрагируют множество деталей и облегчают работу с состояниями. Это одна из самых популярных библиотек такого рода во Flutter. Она активно поддерживается Felix Angelov и другими разработчиками на основе открытого исходного кода.
Судя по названию, Blocs
обрабатывает всю бизнес-логику: будь то взаимодействие с уровнем данных для отображения чего-либо на UI или сложные вычисления.
Но что означает управление состояниями? Разве его нельзя проигнорировать, как, например, на Android или iOS?
Нет, нельзя, поскольку Flutter — декларативный фреймворк. Он строит свой UI для отображения текущего состояния приложения, поэтому при каждом изменении состояния мы перерисовываем UI. По сути, каждому состоянию соответствует свой UI или, как говорится в официальной документации: “UI — это функция состояния приложения”.
Например:
- Состояние “Сеть недоступна” — отображается сообщение в нижней части экрана (snackbar).
- Состояние “Получить данные” — появляется индикатор загрузки.
- Состояние “Данные получены” — отображается виджет
Text
с данными и т. д.
Заметим, что Flutter предлагает множество решений для управления состояниями, но flutter_bloc
— наиболее востребованное среди них.
Добавляем в приложение плагин flutter_bloc
в качестве зависимости. Убедитесь, что у вас в наличии последняя версия библиотеки. Помещаем его в файл проекта pubspec.yml
в раздел зависимостей. Правильно оформляем отступы, как показано ниже:
Добавляем расширение bloc
в IDE (доступно как для VSCode, так и Android Studio), так как оно помогает создавать файлы bloc
и сокращать объем шаблонного кода.
Далее создаем каталог для хранения файлов bloc
, нажимаем на нем Cmd+N
и в выпадающем меню видим опцию Bloc Class
:
Присваиваем имя bloc
. Вы также можете воспользоваться Equitable
. В результате создаются три файла:
Далее мы поочередно разберем каждый из них, но перед этим рассмотрим простую схему для понимания сути данных компонентов и способов их взаимодействия с UI.
Как видим, все просто. Процесс включает 4 этапа.
- UI запускает событие.
Bloc
считывает событие и реализует определенную бизнес-логику.- По завершении вычисления
Bloc
генерирует состояние.
Итак, у нас есть три файла: Bloc
, event
(событие) и state
(состояние). Рассмотрим фрагменты кода. Вы можете пропускать комментарии “Будут использованы позже”, к которым мы вернемся в следующих разделах.
import 'package:bloc/bloc.dart';
part 'login_event.dart';
part 'login_state.dart';
/*
Расширяем класс Bloc и также указываем тип для события и состояния.
*/
class LoginBloc extends Bloc<LoginEvent, LoginState> {
/*
* 1. Это метод конструктора
* Bloc, в случае создания API вы внедрите репозиторий.
* 2. Передаем первое событие в суперкласс,
* это первое сгенерированное событие,
* мы можем применить его в UI для запуска кода настройки.
*/
LoginBloc() : super(LoginInitial()) {
/*
* 1. С помощью этого метода мы регистрируем обработчик события с типом события.
* В данном случае у нас Login Event,
* так как Bloc работает в LoginEvent и LoginState
* 2. Этот метод принимает два параметра Handler и transformer.
* Transformer является необязательным методом, и здесь он отсутствует.
*/
// Примечание: у каждого события должен быть только один обработчик
on<LoginEvent>(_loginEventHandler);
/*
Будет использован позже
on<LoginButtonTappedEvent>(_loginButtonTapped);
on<ShowSnackBarButtonTappedEvent>(_showSnackBarTapped);
*/
}
/*
* 1. Данный метод возвращает Future void, что говорит о его
* способности выполнять асинхронные операции.
* 2. Он принимает два необходимых параметра
* Event и Emitter
*/
Future<void> _loginEventHandler(LoginEvent e, Emitter emit) async {
// Выполнение задачи по реализации бизнес-логики
/*
* У класса Emitter есть ряд других методов,
* но мы выбрали самый простой из них,
* так как нам нужно всего лишь сгенерировать состояние.
*/
emit(LoginInitial());
}
/*
Будет использован позже
Future<void> _loginButtonTapped(LoginButtonTappedEvent e, Emitter emit) async {
emit(UpdateTextState(text: "Text is sent from the Bloc"));
}
Future<void> _showSnackBarTapped(ShowSnackBarButtonTappedEvent e, Emitter emit) async {
emit(ShowSnackbarState());
}*/
}
part of 'login_bloc.dart';
// Это простой абстрактный класс
// который можно расширить для других событий
abstract class LoginEvent {
const LoginEvent();
}
/*
Будет использован позже
class LoginButtonTappedEvent extends LoginEvent {}
class ShowSnackBarButtonTappedEvent extends LoginEvent {}
*/
part of 'login_bloc.dart';
// Это тоже абстрактный класс,
// который можно расширить другими классами
/*
* Один базовый класс нужен для того, чтобы
* на его основе создавать дочерние классы
* в тех файлах, где он используется.
* */
abstract class LoginState {
const LoginState();
}
/*
* Поясним, почему Event является абстрактным классом
* а State - конкретным типом. Дело в том, что
* мы уже создали экземпляр State
* для передачи его суперклассу в bloc,
* но событие мы еще не задействовали.
* Как только это произойдет, мы создадим конкретный класс,
* чтобы инстанцировать и работать с ним.
* */
class LoginInitial extends LoginState {}
/*
Будет использован позже
class UpdateTextState extends LoginState {
final String text;
UpdateTextState({required this.text});
}
class ShowSnackbarState extends LoginState {}
*/
Поскольку фрагменты кода построчно сопровождаются комментариями, вы уже поняли, что именно делают данные файлы. Кроме того, вы также получили наглядное представление этих компонентов в соответствии с ранее рассмотренной схемой архитектуры Bloc
.
Итак, мы определили слой Bloc
. Теперь создадим простой UI и посмотрим, как можно применить LoginBloc
.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class LoginScreen extends StatelessWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Login")),
body: _buildScaffoldBody(),
);
}
Widget _buildScaffoldBody() {
return SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("This will change on button tap"),
const SizedBox(
height: 16,
),
TextButton(
onPressed: () {
// Will do Something interesting
},
child: const Text("Tap me!!!"))
],
),
);
}
}
В UI добавлены текст и кнопка, нажимая на которую мы будем менять текст. Вызываем событие кнопки. Оно получит текст из Bloc
, который мы отобразим.
Но как LoginScreen
узнает о LoginBloc
?
Flutter Blocs уже располагает отличным решением этой задачи. Поскольку во Flutter все делается с помощью виджетов, Flutter Blocs предлагает множество полезных из них. Работая с примерами, мы познакомимся с некоторыми из этих виджетов в следующем разделе.
Перед началом работы с Bloc
ответим на 3 вопроса.
1. Как сделать Bloc доступным для дерева виджетов (UI)?
Такую функциональность обеспечивает виджет BlocProvider
. Благодаря ему Bloc
становится доступным для всего дерева виджетов, которое передается в параметре child
:
Widget _prepareLoginScreen() {
return BlocProvider(
create: (BuildContext context) {
return LoginBloc();
},
child: const LoginScreen(),
);
}
В представленном коде мы создаем новый экземпляр LoginBloc
и в качестве дочернего элемента передаем экземпляр LoginScreen
. Bloc Provider
предоставляет дереву виджетов LoginScreen
доступ к LoginBloc
и автоматически обрабатывает закрытие Bloc
.
2. Как вызвать событие из UI в ответ на действие?
Поскольку Bloc
доступен в дереве виджетов, то можно обратиться к нему откуда угодно. Для этого существуют 2 способа:
a. посредством контекста (context):
context.read<BlocA>();
b. посредством Bloc Provider
:
BlocProvider.of<BlocA>(context)
В чем отличие? И каковы их случаи применения?
Оба способа почти идентичны и практически ничем не отличаются. Однако первый из них определяется как расширение в BuildContext
, что упрощает взаимодействие с ним. Для дальнейшей работы выбирайте любой из них.
Добавляем в Bloc
событие при нажатии на кнопку (ButtonTappedEvent
):
TextButton(
onPressed: () {
context.read<LoginBloc>().add(LoginButtonTappedEvent());
},
child: const Text("Tap me!!!"),
)
Примечание. Важно передать корректный тип
Bloc
, так как он просматривает дерево в поисках экземпляра этого типа.
Посмотрите, насколько просто запустить событие из UI. Нужно лишь вызвать метод add
в Bloc
и передать экземпляр Event
.
Теперь вернемся к Bloc
и проверим, зарегистрировали ли мы событие, отправленное из UI. Получив его, мы генерируем State
со String
для отображения в UI. Можно обратиться к представленному ранее коду Bloc
и просто раскомментировать его части с комментарием “Will be used later” (“Будет использован позже”).
3. Как обновляется UI при генерации нового состояния?
Как вы могли догадаться, для этого тоже есть виджет, который называется BlocConsumer
:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_blocs_testing/bloc_tutorial/bloc/login_bloc.dart';
class LoginScreen extends StatelessWidget {
const LoginScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Login")),
body: _buildScaffoldBody(),
);
}
Widget _buildScaffoldBody() {
return BlocConsumer<LoginBloc, LoginState>(
builder: (context, state) {
return _buildParentWidget(context, state);
},
listener: (context, state) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('This is a snack bar!!!!'),
));
},
buildWhen: (previous, current) => _shouldBuildFor(current),
listenWhen: (previous, current) => _shouldListenFor(current),
);
}
bool _shouldListenFor(LoginState currentState) {
return currentState is ShowSnackbarState;
}
bool _shouldBuildFor(LoginState currentState) {
return currentState is LoginInitial || currentState is UpdateTextState;
}
Widget _buildParentWidget(BuildContext context, LoginState state) {
return SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildTextWidget(state),
const SizedBox(
height: 16,
),
TextButton(
onPressed: () {
context.read<LoginBloc>().add(LoginButtonTappedEvent());
},
child: const Text("Tap me!!!"),
),
const SizedBox(
height: 16,
),
TextButton(
onPressed: () {
context
.read<LoginBloc>()
.add(ShowSnackBarButtonTappedEvent());
},
child: const Text("Show Snackbar"))
],
),
);
}
Widget _buildTextWidget(LoginState state) {
if (state is UpdateTextState) {
return Text(state.text);
}
else {
return const Text("This will change on button tap");
}
}
}
Рассмотрим BlocConsumer
. Хотя представленный выше пример содержит весь LoginScreen
, мы сосредоточимся только на функции _buildScaffoldBody
.
Как видно, BlocConsumer
имеет 4 аргумента. Пройдемся по каждому из них.
builder
. Перестраивает дерево виджетов в ответ на изменения состояния. Это значит, что каждый раз при создании нового состояния происходит перестройка виджетов. В результате вы можете воспользоваться значением, которое передается в состояние, генерируемое в Bloc
. В данном примере мы передали текст из Bloc
в UpdateTextState
и применили его в UI. Это состояние порождается всякий раз при нажатии на кнопку “Tap me!!!”(“Нажми меня!!!”).
listener
. Каждый раз, когда генерируется состояние, вызывается слушатель listener
. Однако в отличие от builder
он не возвращает виджет. Как правило, мы задействуем слушателя единожды, поскольку он вызывается один раз при каждом изменении состояния. Например, при отображении сообщения внизу экрана, диалогового окна, всплывающей информационной панели или переходе к следующему экрану.
buildWhen
. Это необязательный параметр, который предоставляет предыдущее и текущее состояние и возвращает логическое значение. Если мы возвращаем true
, он вызывает builder
, в противном же случае этого не происходит. Если данный параметр отсутствует, builder
вызывается при каждом изменении состояния.
listenWhen
. Аналогичен buildWhen
, но отвечает за управление Listener
. Если возвращается true
, вызывается слушатель.
В данном примере Listener
нужен для состояния ShowSnackBar
, которое отображает сообщение с текстом, появляющимся при нажатии на кнопку. builder
предназначен для LoginInitial
и UpdateTextState
, которые обновляют текст на экране при нажатии на кнопку.
Отметим еще один интересный момент. BlocConsumer
принимает необязательный параметр bloc
. Если вы его не предоставляете, он пытается выполнить поиск с помощью BlocProvider
или текущего BuildContext
. Таким образом, если вы не внедрили Bloc
(вопрос №1), BlocConsumer
выбросит исключение.
В практической деятельности вам потребуется либо builder
, либо listener
. Для таких случаев существуют различные виджеты BlocBuilder
и BlocListener
, с которыми стоит познакомиться. Помимо них, Flutter blocs предоставляет и другие не менее полезные виджеты.
Подведем итоги
- Строим UI (для реализации бизнес-логики).
- Создаем
Bloc
(классыBloc
,Event
иState
). - Связываем их вместе (не забывайте о трех вопросах).
- Задействуем
BlocProvider
для инициализацииBloc
и делаем его доступным для дерева виджетов. - Применяем контекст для поиска
Bloc
, чтобы добавить событие. - Выполняем бизнес-логику в
Bloc
и выдаем результат. - Используем
BlocConsumer
,BlocBuilder
иBlocListener
для прослушивания изменений состояния и выполнения соответствующих действий.
Полезные ресурсы
Читайте также:
- Даешь меньше ошибок в проектах ПО!
- 5 причин выбрать Flutter
- Адаптивный дизайн на разных уровнях Flutter
Читайте нас в Telegram, VK и Дзен
Перевод статьи Sumit Ghosh: Flutter Blocs: A Simple Introduction