Flutter — динамично развивающийся фреймворк для написания кроссплатформенных приложений. Несмотря на свою молодость (первая стабильная версия Flutter 1.0 была представлена всего лишь 4 декабря 2018 года, то есть чуть больше года назад на момент публикации статьи), он приобрел немало сторонников, и его популярность только растет. Сегодня мы хотели бы рассказать об одной из самых популярных среди
Оглавление
Что такое BLoC
Давайте разберемся с основной терминологией. BLoC — это акроним от «Business Logic Component» (компонент
Обратите внимание на разницу в написании терминов. Здесь и далее употребляется BLoC, когда речь идет об архитектуре, и Bloc – когда о классе в программе.
Отделение UI от
Одной из отличительных особенностей паттерна является то, что он полностью базируется на реактивности. Что это значит? Реактивное программирование — это программирование с асинхронными потоками данных. В традиционном императивном стиле мы обычно пишем код следующего содержания: вот текстовое поле, назначаем ему обработчик, реагирующий на ввод нового символа, в обработчике добавляем объект, у которого вызываем метод обновления текстового поля в модели. Мы четко описываем каждый шаг. В реактивном подходе все выглядит немного иначе. Текстовое поле связывается с конкретной переменной в модели. И как только пользователь начинает
Для реализации реактивных сценариев язык Dart по умолчанию предлагает класс StreamController. Он позволяет нам моделировать поток данных с помощью двух составляющих — sink (входной поток, куда пользователь добавляет события) и stream (выходной поток, который слушают один или несколько объектов и реагируют на изменения). Пример описания
final StreamController<String>_nameController = StreamController<String>();
// описываем геттер для входного потока (sink)
Sink<String> get inName =>_nameController.sink;
// описываем геттер для выходного потока (stream)
Stream<String> get outName =>_nameController.stream;
Отдельные геттеры удобно делать по следующим причинам.
Код метода build для вашего виджета может выглядеть так:
@override
Widget build(BuildContext context) {
return StreamBuilder<String> (
stream: someBloc.outName,
builder: (BuildContext context, AsyncSnapshot<String>snapshot) => Text(snapshot.data)
);
}
Здесь мы создаем специальный виджет StreamBuilder. Первым аргументом указываем поток, который поставляет данные нашему виджету, вторым — функцию, «собирающую» виджет на основе отрисовочного контекста и данных, асинхронно приходящих из потока.
Для реализации BLoC часто используются уже готовые библиотеки, например, эта. Нашей же команде она показалась слишком тяжеловесной, поэтому мы воспользовались кастомной, упрощенной реализацией, описанной в этой статье. Рассмотрим ее основные положения.
Знакомство с BlocProvider
Каждый Bloc поддерживает общий интерфейс BlocBase, содержащий в себе только один обязательный к реализации метод dispose. Он нужен, чтобы не забыть реализовать освобождение ресурсов, занятых
abstract class BlocBase {
void dispose();
}
Для того чтобы вы могли получить доступ к
class BlocProvider<T extends BlocBase>extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
@override
_BlocProviderState<T>createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
@override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return widget.child;
}
}
Идея заключается в том, что наш провайдер, точно такой же виджет, который состоит из двух слагаемых: Bloc, к которому будут обращаться виджеты, и дерево виджетов, которое мы видим на экране. Для доступа к
@override
Widget build(BuildContext context) {
MyBloc bloc = BlocProvider.of(context);
return Scaffold(body: /*ваша реализация с использованием StreamBuilder*/);
}
Доступ к
Небольшое уточнение по поводу оптимизации. Поиск по иерархии виджетов имеет вычислительную сложность O (n). Если ваш провайдер расположен относительно близко к виджету, который запросил Bloc, такая операция не займет много времени. Однако в случае, когда ваш виджет имеет большую глубину вложенности, а доступ к
Пример реализации
Рассмотрим, как работа с
У MainBloc есть входной поток с UI событиями (в нашем случае нажатия на кнопку «+») и выходной поток со значением счетчика нажатий. Само значение счетчика counter из State должно переехать сюда, поскольку оно — часть
import 'dart:async';
import 'package:simple_bloc_app/bloc/common/bloc_base.dart';
enum MainBlocEvent {
incrementCounter
// ...другие события, которые будет обрабатывать Bloc
}
class MainBloc extends BlocBase {
// данные Bloc-а
int _counter = 0;
// stream controllers
final StreamController<int> _counterController = StreamController<int>();
final StreamController<MainBlocEvent> _eventController =
StreamController<MainBlocEvent>();
Sink<int> get _inCounter => _counterController.sink;
Stream<int> get outCounter => _counterController.stream;
Sink<MainBlocEvent> get inEvent => _eventController.sink;
Stream<MainBlocEvent> get _outEvent => _eventController.stream;
MainBloc() {
// подписываемся на поток
// здесь обрабатываются события, пришедшие со стороны UI
_outEvent.listen(_handleEvent);
}
// альтернатива потоку с UI-событиями
void onIncrementButton() {
_handleIncrementCounterEvent();
}
@override
void dispose() {
// здесь мы закрываем открытые контроллеры
_eventController.close();
_counterController.close();
}
void _handleEvent(MainBlocEvent event) {
switch (event) {
case MainBlocEvent.incrementCounter:
_handleIncrementCounterEvent();
break;
default:
// чтобы гарантировать, что мы не пропустим ни один кейс enum-а
assert(false, "Should never reach there");
break;
}
}
void _handleIncrementCounterEvent() {
_inCounter.add(++_counter);
}
}
Если создание отдельного потока для
Теперь вынесем код виджета из main.dart в MainScreen и интегрируем в него наш MainBloc:
import 'package:flutter/material.dart';
import 'package:simple_bloc_app/bloc/common/bloc_provider.dart';
import 'package:simple_bloc_app/bloc/main_bloc.dart';
class MainScreen extends StatefulWidget {
@override
State<StatefulWidget>createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
@override
Widget build(BuildContext context) {
final MainBloc bloc = BlocProvider.of(context);
return Scaffold(
appBar: AppBar(
title: Text("My first BLoC app"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: bloc.outCounter,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return Text(
"${snapshot.data ?? 0}",
style: Theme.of(context).textTheme.display1,
);
},
)
],
),
),
floatingActionButton: FloatingActionButton(
// используйте либо поток, либо вызов метода Bloc-а
onPressed: () => bloc.inEvent.add(MainBlocEvent.incrementCounter),
//onPressed: () => bloc.onIncrementButton(),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Обратите внимание, текстовый виджет со значением счетчика теперь находится в StreamBuilder, который подписан на поток из
Наконец, создадим BlocProvider для нашего экрана. Вынесем этот код в main.dart:
import 'package:flutter/material.dart';
import 'package:simple_bloc_app/bloc/common/bloc_provider.dart';
import 'package:simple_bloc_app/bloc/main_bloc.dart';
import 'package:simple_bloc_app/screen/main_screen.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
// создаем Bloc и экран для него
final MainBloc bloc = MainBloc();
final MainScreen screen = MainScreen();
// на их основе создаем BlocProvider
return BlocProvider(
child: screen,
bloc: bloc,
);
}
}
Запускаем приложение и проверяем его работоспособность.
Код проекта вы можете посмотреть здесь.
Чек-лист по работе с BLoC-архитектурой
Итак, алгоритм работы при создании новых экранов с использованием
- Создать Bloc для конкретного экрана.
- Описать его входы и выходы в виде потоков.
- Реализовать требуемую
бизнес-логику. - Создать новый экран.
- Поместить в корень дерева виджетов BlocProvider, передать ему экземпляр реализованного
Bloc-а и, собственно, виджеты, которые будут отображаться на экране. - Реализовать экран, используя по необходимости
StreamBuilder-ы , подписанные на выходы изBloc-а.
Разумеется, никто не заставляет вас ограничивать себя исключительно