Главное Свежее Вакансии Образование
Выбор редакции:
314 0 В избр. Сохранено
Авторизуйтесь
Вход с паролем

Разделение бизнес-логики и UI во Flutter с помощью BLoC-архитектуры

Сегодня мы хотели бы рассказать об одной из самых популярных среди Flutter-разработчиков архитектур, которая была разработана в Google, и называется — BLoC.

Что такое BLoC


Давайте разберемся с основной терминологией. BLoC — это акроним от «Business Logic Component» (компонент бизнес-логики). Как следует из названия, это класс, отделяющий бизнес-логику приложения от пользовательского интерфейса. Такой компонент содержит код, который можно повторно использовать где угодно: в другом модуле, на другой платформе, в другом приложении. Если вы до этого работали с архитектурой MVVM, то можете провести аналогию и сравнить Bloc с ViewModel — они похожи по своему назначению.

Обратите внимание на разницу в написании терминов. Здесь и далее употребляется BLoC, когда речь идет об архитектуре, и Bloc — когда о классе в программе.

Отделение UI от бизнес-логики для Flutter жизненно необходимо. Согласитесь, что карабкаться вверх-вниз по дереву виджетов (UI-компонентов) в поисках нужной вам логики — не самая приятная вещь. Особенно, если верстка и так содержит очень много кода и разбросана по разным файлам. Кроме того, если мы следуем заветам Clean Architecture (а мы им следуем, не так ли?), то в нашем UI вообще не должно быть ничего лишнего. Равно как и бизнес-логика ничего не должна знать про UI, который к ней обращается. Благодаря такому тщательному разделению ответственностей мы получаем полностью изолированный компонент, который можно легко тестировать независимо от UI и использовать в другом окружении. Этот компонент и есть наш Bloc.

Одной из отличительных особенностей паттерна является то, что он полностью базируется на реактивности. Что это значит? Реактивное программирование — это программирование с асинхронными потоками данных. В традиционном императивном стиле мы обычно пишем код следующего содержания: вот текстовое поле, назначаем ему обработчик, реагирующий на ввод нового символа, в обработчике добавляем объект, у которого вызываем метод обновления текстового поля в модели. Мы четко описываем каждый шаг. В реактивном подходе все выглядит немного иначе. Текстовое поле связывается с конкретной переменной в модели. И как только пользователь начинает что-то печатать, эта переменная сразу принимает то значение, которое сейчас содержится в текстовом поле.

Для реализации реактивных сценариев язык Dart по умолчанию предлагает класс StreamController. Он позволяет нам моделировать поток данных с помощью двух составляющих — sink (входной поток, куда пользователь добавляет события) и stream (выходной поток, который слушают один или несколько объектов и реагируют на изменения). Пример описания входа-выхода для экрана в коде Bloc-а выглядит следующим образом:


final StreamController_nameController = StreamController();

// описываем геттер для входного потока (sink)

Sink get inName =>_nameController.sink;

// описываем геттер для выходного потока (stream)

Stream get outName =>_nameController.stream;

Отдельные геттеры удобно делать по следующим причинам. Во-первых, так гораздо проще, чем каждый раз писать nameController.sink, во-вторых, вы даете входу/выходу осмысленное имя, что позволяет легче понять его назначение и тем самым улучшает читабельность.

Код метода build для вашего виджета может выглядеть так:


@override

Widget build(BuildContext context) {

return StreamBuilder (

stream: someBloc.outName,

builder: (BuildContext context, AsyncSnapshotsnapshot) => Text(snapshot.data)

);

}

Здесь мы создаем специальный виджет StreamBuilder. Первым аргументом указываем поток, который поставляет данные нашему виджету, вторым — функцию, «собирающую» виджет на основе отрисовочного контекста и данных, асинхронно приходящих из потока.

Для реализации BLoC часто используются уже готовые библиотеки, например, эта. Нашей же команде она показалась слишком тяжеловесной, поэтому мы воспользовались кастомной, упрощенной реализацией, описанной в этой статье. Рассмотрим ее основные положения.

Знакомство с BlocProvider


Каждый Bloc поддерживает общий интерфейс BlocBase, содержащий в себе только один обязательный к реализации метод dispose. Он нужен, чтобы не забыть реализовать освобождение ресурсов, занятых Bloc-ом, например, закрыть активные потоки. Выглядит он так:


abstract class BlocBase {

void dispose();

}

Для того чтобы вы могли получить доступ к Bloc-у из любого узла дерева виджетов, корнем этого дерева становится такая сущность, как BlocProvider. Рассмотрим ее код:


class BlocProviderextends StatefulWidget {

BlocProvider({

Key key,

@required this.child,

@required this.bloc,

}): super(key: key);

final T bloc;

final Widget child;

@override

_BlocProviderStatecreateState() => _BlocProviderState();

static T of(BuildContext context){

final type = _typeOf>();

BlocProvider provider = context.ancestorWidgetOfExactType(type);

return provider.bloc;

}

static Type _typeOf() => T;

}

class _BlocProviderState extends State>{

@override

void dispose(){

widget.bloc.dispose();

super.dispose();

}

@override

Widget build(BuildContext context){

return widget.child;

}

}

Идея заключается в том, что наш провайдер, точно такой же виджет, который состоит из двух слагаемых: Bloc, к которому будут обращаться виджеты, и дерево виджетов, которое мы видим на экране. Для доступа к Bloc-у в методе виджета build вам нужно написать так:


@override

Widget build(BuildContext context) {

MyBloc bloc = BlocProvider.of(context);

return Scaffold(body: /*ваша реализация с использованием StreamBuilder*/);

}

Доступ к Bloc-у под капотом реализован очень просто. Мы движемся вверх по иерархии виджетов в рамках BuildContext, пока не встретим BlocProvider. Как только это произошло, мы можем вернуть Bloc этого провайдера.

Небольшое уточнение по поводу оптимизации. Поиск по иерархии виджетов имеет вычислительную сложность O (n). Если ваш провайдер расположен относительно близко к виджету, который запросил Bloc, такая операция не займет много времени. Однако в случае, когда ваш виджет имеет большую глубину вложенности, а доступ к Bloc-у ему требуется часто, это может сказаться на производительности. Тогда есть смысл подумать о том, чтобы кэшировать ссылку на Bloc, например, передавая его как аргумент в конструктор виджета.

Пример реализации


Рассмотрим, как работа с BLoC-архитектурой может выглядеть на практике. Перепишем пример, который по умолчанию генерируется при создании Flutter-проекта. Так как здесь всего один экран, у нас будет один виджет MainScreen с прилагающимся к нему MainBloc.

У MainBloc есть входной поток с UI событиями (в нашем случае нажатия на кнопку «+») и выходной поток со значением счетчика нажатий. Само значение счетчика counter из State должно переехать сюда, поскольку оно — часть бизнес-логики. В результате получаем следующий код для MainBloc:


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 _counterController = StreamController();

final StreamController _eventController =

StreamController();

Sink get _inCounter => _counterController.sink;

Stream get outCounter => _counterController.stream;

Sink get inEvent => _eventController.sink;

Stream 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);

}

}

Если создание отдельного потока для UI-событий вам кажется излишним, то вы можете написать отдельный метод-обработчик нажатия на кнопку «+» (onIncrementButton) и вызывать его напрямую.

Теперь вынесем код виджета из 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

StatecreateState() => _MainScreenState();

}-

class _MainScreenState extends State {

@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: [

Text(

’You have pushed the button this many times:’,

),

StreamBuilder(

stream: bloc.outCounter,

builder: (BuildContext context, AsyncSnapshot 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, который подписан на поток из Bloc-а. При нажатии на кнопку в Bloc прокидывается соответствующее событие (на ваше усмотрение, через поток или обработчик). Bloc обрабатывает событие, увеличивая хранящийся в нем счетчик нажатий на 1. После чего значение этого счетчика передается в другой поток, на который подписан виджет на стороне UI. StreamBuilder получает новое состояние из потока (AsyncSnapshot) и перерисовывает свое содержимое с новыми данными.

Наконец, создадим 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 {

@override

Widget build(BuildContext context) {

// создаем Bloc и экран для него

final MainBloc bloc = MainBloc();

final MainScreen screen = MainScreen();

// на их основе создаем BlocProvider

return BlocProvider(

child: screen,

bloc: bloc,

);

}

}

Запускаем приложение и проверяем его работоспособность.


Код проекта вы можете посмотреть здесь.

Чек-лист по работе с BLoC-архитектурой


Итак, алгоритм работы при создании новых экранов с использованием BLoC-архитектуры выглядит следующим образом:

  1. Создать Bloc для конкретного экрана.
  2. Описать его входы и выходы в виде потоков.
  3. Реализовать требуемую бизнес-логику.
  4. Создать новый экран.
  5. Поместить в корень дерева виджетов BlocProvider, передать ему экземпляр реализованного Bloc-а и, собственно, виджеты, которые будут отображаться на экране.
  6. Реализовать экран, используя по необходимости StreamBuilder-ы, подписанные на выходы из Bloc-а.

Разумеется, никто не заставляет вас ограничивать себя исключительно StreamBuilder-ами. Более того, в случае, когда для отрисовки виджета вам нужно просто один раз синхронно получить данные из Bloc-а, «плодить» stream-ы даже нерационально. А для разовой подгрузки данных по сети можно использовать, например, FutureBuilder. Поэтому изучайте, экспериментируйте, создавайте. В следующей статье мы расскажем, как наша команда переходила от привычного паттерна VIPER к BLoC и с какими сложностями мы при этом столкнулись.

0
В избр. Сохранено
Авторизуйтесь
Вход с паролем
Комментарии
Выбрать файл
Блог проекта
Расскажите историю о создании или развитии проекта, поиске команды, проблемах и решениях
Написать
Личный блог
Продвигайте свои услуги или личный бренд через интересные кейсы и статьи
Написать

Spark использует cookie-файлы. С их помощью мы улучшаем работу нашего сайта и ваше взаимодействие с ним.