Реактивное программирование в трепетаре: пары и блоки на практике

Flutter

оригинал:Reactive Programming - Streams - BLoC - Practical Use CasesавторDidier BoelensдляReactive Programming - Streams - BLoCписьменное продолжение

Перед прочтением этой статьи рекомендуется прочитать предыдущую статью.Существуют две версии китайского перевода первой статьи:

  1. [Перевод] Реактивное программирование на Flutter: потоки и BLoC by JarvanMo

    более верный оригиналу

  2. Как использовать StreamBuilder и BLoC для управления состоянием виджета во Flutter by Йошихара Рамен

    Некоторые основные концепции опущены и добавлены некоторые личные интерпретации.

предисловие

в пониманииBLoC, Reactive Programmingа такжеStreamsПосле концепции мне потребовалось некоторое время, чтобы продолжить исследования, и теперь я очень рад поделиться с вами некоторыми шаблонами, которые я часто использую и которые очень полезны (по крайней мере, я так думаю). Эти шаблоны экономят мне много времени на разработку и облегчают чтение и отладку кода.

В этой статье я хочу поделиться:

  1. BlocProviderоптимизация производительности

    Объединение преимуществ StatefulWidget и InheritedWidget для создания BlocProvider

  2. BLoCмасштаб и инициализация

    Инициализировать BLoC в соответствии с областью использования BLoC.

  3. Управление событиями и состояниями

    Реакция на изменение состояния на основе событий

  4. проверка формы

    Управление поведением формы на основе проверки элемента формы (пример включает сравнение общих паролей и повторяющихся паролей в формах).

  5. Часть режима

    Позволяет компоненту адаптировать свое поведение в зависимости от среды (независимо от того, находится ли он в списке/коллекции/компоненте).

Полный код, задействованный в этой статье, можно найти по адресуGitHubПроверить.

1. BlocProviderоптимизация производительности

Я хочу познакомить вас с моей комбинациейInheritedWidgetвыполнитьBlocProviderНовая схема, таким образом, по сравнению с исходной, основанной наStatefulWidgetТо, как это реализовано, имеет преимущества в производительности.

1.1. Старая реализация BlocProvider

Раньше я основывался на обычномStatefulWidgetреализоватьBlocProvider, код выглядит следующим образом:

bloc_provider_previous.dart

abstract class BlocBase {
  void dispose();
}

// Generic BLoC provider
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;
  }
}

Преимущества этой схемы:StatefulWidgetизdispose()способ убедиться, чтоBLoCРесурсы памяти, выделенные во время инициализации, могут быть освобождены, когда они не нужны.

Примечание переводчика

Это преимущество основано исключительно наInheritedWidgetтрудно достичь, потому чтоInheritedWidgetне предоставленdisposeМетод, а дартский язык не имеет независимого деструктора

Хотя это решение работает нормально, оно не является оптимальным с точки зрения производительности.

Это потому чтоcontext.ancestorWidgetOfExactType()это временная сложностьO(n)метод, чтобы получитьancestor, он будет следовать дереву представления от текущегоcontextНачните рекурсивно повышаться, чтобы найти егоparentсоответствует указанному типу. если текущийcontextи цельancestorЭтот способ приемлем, если расстояние недалеко, в противном случае его следует по возможности избегать.

Вот исходный код, определяющий этот метод во Flutter:

@override
Widget ancestorWidgetOfExactType(Type targetType) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;
    while (ancestor != null && ancestor.widget.runtimeType != targetType)
        ancestor = ancestor._parent;
    return ancestor?.widget;
}

1.2. Новая реализация BlocProvider

Хотя новая схема в основном основана наStatefulWidgetреализуется, но сочетает в себеInheritedWidget

Примечание переводчика

в оригиналеStatefulWidgetизchildУпакуйте еще одинInheritedWidget

Вот реализованный код:

bloc_provider_new.dart

Type _typeOf<T>() => T;

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 Widget child;
  final T bloc;

  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context){
    final type = _typeOf<_BlocProviderInherited<T>>();
    _BlocProviderInherited<T> provider = 
            context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
    return provider?.bloc;
  }
}

class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>>{
  @override
  void dispose(){
    widget.bloc?.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context){
    return new _BlocProviderInherited<T>(
      bloc: widget.bloc,
      child: widget.child,
    );
  }
}

class _BlocProviderInherited<T> extends InheritedWidget {
  _BlocProviderInherited({
    Key key,
    @required Widget child,
    @required this.bloc,
  }) : super(key: key, child: child);

  final T bloc;

  @override
  bool updateShouldNotify(_BlocProviderInherited oldWidget) => false;
}

Нет сомнений в том, что новая схема имеет преимущества в производительности, поскольку использованиеInheritedWidget, при поиске совпадения с указанным типомancestorКогда мы сможем позвонитьInheritedWidgetметод экземпляраcontext.ancestorInheritedElementForWidgetOfExactType(), а временная сложность этого методаO(1), а это означает, что почти сразу можно найтиancestor.

Исходный код определения этого метода во Flutter отражает это:

@override
InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null 
                                    ? null 
                                    : _inheritedWidgets[targetType];
    return ancestor;
}

Конечно, это также связано с тем, что Flter Framework кэширует всеInheritedWidgetsЯ смог добиться.

зачем использоватьancestorInheritedElementForWidgetOfExactTypeвместоinheritFromWidgetOfExactType ?

потому чтоinheritFromWidgetOfExactTypeНе только найти, чтобы получить указанный типWidget, будет такжеcontextзарегистрироваться наWidget,так чтоWidgetПосле измененияcontextМожно получить новое значение;

Это не то, что мы хотим, все, что мы хотим, это соответствовать указанному типуWidget(то есть,BlocProvider) Только.

1.3 Как пользоваться новой схемой BlocProvider?

1.3.1. Внедрение BLoC

Widget build(BuildContext context){
    return BlocProvider<MyBloc>{
        bloc: myBloc,
        child: ...
    }
}

1.3.2 Получение BLoC

Widget build(BuildContext context){
    MyBloc myBloc = BlocProvider.of<MyBloc>(context);
    ...
}

2. BLoCмасштаб и инициализация

Чтобы ответить "Где инициализироватьBLoC? «Этот вопрос нужно сначала прояснить.BLoCдоступный диапазон(scope).

2.1 Доступно в любом месте приложения

В практических приложениях часто необходимо обрабатывать данные или состояния, которые должны быть доступны любому компоненту в приложении, например аутентификация пользователя, профили пользователей, настройки пользователя, корзины покупок и т. д. Вот два метода, которые применимы к эта ситуация.BLoCстроить планы:

2.1.1 Глобальный синглтон (Global Singleton)

В этой схеме используетсяWidgetв дереве просмотраGlobalобъект, доступный после инстанцирования для всехWidgetиспользовать.

bloc_singleton.dart

import 'package:rxdart/rxdart.dart';

class GlobalBloc {
  ///
  /// Streams related to this BLoC
  ///
  BehaviorSubject<String> _controller = BehaviorSubject<String>();
  Function(String) get push => _controller.sink.add;
  Stream<String> get stream => _controller;

  ///
  /// Singleton factory
  ///
  static final GlobalBloc _bloc = new GlobalBloc._internal();
  factory GlobalBloc(){
    return _bloc;
  }
  GlobalBloc._internal();
  
  ///
  /// Resource disposal
  ///
  void dispose(){
    _controller?.close();
}

GlobalBloc globalBloc = GlobalBloc();

Использовать глобальный синглтонBLoC,нужно толькоimportЗатем вызовите определенный метод:

import 'global_bloc.dart';

class MyWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        globalBloc.push('building MyWidget'); //调用 push 方法添加数据 
        return Container();
    }
}

если ты хочешь одинТолько, доступный из любого компонента в приложенииBLoCЕсли это так, это решение все еще хорошо, потому что:

  • Легко использовать
  • не зависит ни от чегоBuildContext
  • конечно не надо проходитьcontextнайтиBlocProviderспособ получитьBLoC
  • Освободить ресурсы тоже очень просто, достаточно поставитьapplication Widgetна основеStatefulWidgetреализацию, а затем переписать ееdispose()метод, вdispose()вызыватьglobalBloc.dispose()Только что

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

2.1.2 Внедрение в верхний уровень дерева представления

Во Flutter содержит все страницыancestorсам должен бытьРодитель MaterialApp. Это связано с тем, что страница (илиRoute) фактически используется как все страницыStackодин из предметов, содержащихся вOverlayEntryсередина.

Другими словами, каждая страница имеет свой собственныйнезависимо от любой другой страницыизBuildcontext.这也解释了为啥不用任何技巧是没办法实现两个页面(或路由)之间数据共享的。

Следовательно, это должно бытьBlocProviderтак какMaterialAppРодитель приложения может использоваться в любом месте приложения.BLoC,Следующим образом:

bloc_on_top.dart

void main() => runApp(Application());

class Application extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<AuthenticationBloc>(
      bloc: AuthenticationBloc(),
      child: MaterialApp(
        title: 'BLoC Samples',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: InitializationPage(),
      ),
    );
  }
}

2.2 Доступно в дереве подвидов (несколько страниц или компонентов)

В большинстве случаев нам нужно использовать его только в части дерева страниц/компонентов приложения.BLoC.举个例子,在一个 App 中有类似论坛的功能模块,在这个功能模块中我们需要用到BLoCреализовать:

  • Взаимодействуйте с внутренними серверами, чтобы получать, добавлять и обновлять сообщения.
  • Список данных, которые будут отображаться на определенной странице

Очевидно, нам не нужноBLoCРеализован как глобально доступный, он должен быть доступен только в дереве просмотра, связанном с форумом.

затем черезBlocProviderБудуBLoCСпособ внедрения в качестве корня (родителя) поддерева модулей, например:

bloc_init_root.dart

class MyTree extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    return BlocProvider<MyBloc>(
      bloc: MyBloc(),
      child: Column(
        children: <Widget>[
          MyChildWidget(),
        ],
      ),
    );
  }
}

class MyChildWidget extends StatelessWidget {
  @override 
  Widget build(BuildContext context){
    MyBloc = BlocProvider.of<MyBloc>(context);
    return Container();
  }
}

Таким образом, все под этим модулемWidgetможно сделать, позвонивBlocProvider.ofполучитьBLoC.

Уведомление

Вышеупомянутое не лучшее решение, потому что каждый разMyTreeПереинициализировать при рефакторинге (перестроить)BLoC, результат:

  • потерянныйBLoCсодержимое данных, которое уже существует в
  • повторно инициализироватьBLoCпотреблять процессорное время

В этом примере лучше использоватьStatefulWidget, используя свою настойчивостьStateХарактеристики для решения вышеуказанных проблем, код выглядит следующим образом:

bloc_init_root_2.dart

class MyTree extends StatefulWidget {
 @override
  _MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree>{
  MyBloc bloc;
  
  @override
  void initState(){
    super.initState();
    bloc = MyBloc();
  }
  
  @override
  void dispose(){
    bloc?.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context){
    return BlocProvider<MyBloc>(
      bloc: bloc,
      child: Column(
        children: <Widget>[
          MyChildWidget(),
        ],
      ),
    );
  }
}

Если так, даже еслиMyTreeРефакторинг компонентов и отсутствие повторной инициализацииBLoC, но напрямую использовать предыдущийBLoCпример.

2.3 Доступен в виде одного компонента

Если только в определенном компоненте(Widget)используется вBLoC, просто нужно построить внутри этого компонентаBLoCПросто пример.

3. Управление событиями и состояниями (Event - State)

Иногда нам нужно закодировать и реализовать некоторые сложные бизнес-процессы.Эти процессы могут состоять из последовательных или параллельных, трудоемких, синхронных или асинхронных подпроцессов.Вполне вероятно, что результаты обработки каждого подпроцесса также постоянно меняются , а обновления View также могут потребоваться в зависимости от хода их обработки или состояния.

Цель решения «управление событиями и состояниями» в этой статье — упростить работу с такими сложными бизнес-процессами.

Сценарии основаны на следующих процессах и правилах:

  • генерировать событие
  • Событие запускает какое-то действие(action), эти действия вызывают создание/изменение одного или нескольких состояний
  • Эти состояния, в свою очередь, запускают другие события или генерируют/переходят в другие состояния.
  • Затем эти события запускают другие действия, основанные на изменении состояния.
  • так далее…

Чтобы лучше продемонстрировать эти концепции, я также приведу два конкретных примера:

  • Инициализация приложения(Application initialization)

    Много раз нам нужно инициализировать приложение, выполнив ряд действий, которые могут быть связаны с взаимодействием с сервером (например, выборка и загрузка некоторых данных). А во время процесса инициализации также может понадобиться отобразить индикатор выполнения и анимацию загрузки, чтобы пользователь мог терпеливо ждать.

  • Аутентификация пользователя(Authentication)

    После запуска приложения пользователю необходимо авторизоваться или зарегистрироваться.После успешного входа пользователь перейдет (перенаправит) на главную страницу приложения, а когда пользователь выйдет из системы, он перейдет (перенаправит) на страницу проверки.

Чтобы иметь дело со всеми возможностями, мы будем управлять серией событий, и эти события могут запускаться в любом месте приложения, что чрезвычайно усложняет управление событиями и состояниями.К счастью, мы можем использовать комбинациюBlocEventStateBuiderизBlocEventStateКлассы значительно снижают сложность управления событиями и состояниями.

3.1. BLOCEVENTSTATE Абстрактный класс

BlocEventStateЛогика заключается в том, чтобыBLoCОпределяется как такой набор механизмов:

  • получать события(event)как ввод
  • Когда запускается новое событие (ввод), вызовите соответствующий обработчик событийeventHandler
  • обработчик события(eventHandler)ответственный за мероприятие(event)принимать соответствующие меры(actions)После, бросая одно или несколько состояний(State)как ответ

Как показано ниже:

BlocEventState

определениеBlocEventStateКод и описание следующие:

bloc_event_state.dart

import 'package:blocs/bloc_helpers/bloc_provider.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';

abstract class BlocEvent extends Object {}
abstract class BlocState extends Object {}

abstract class BlocEventStateBase<BlocEvent, BlocState> implements BlocBase {
  PublishSubject<BlocEvent> _eventController = PublishSubject<BlocEvent>();
  BehaviorSubject<BlocState> _stateController = BehaviorSubject<BlocState>();

  ///
  /// To be invoked to emit an event
  ///
  Function(BlocEvent) get emitEvent => _eventController.sink.add;

  ///
  /// Current/New state
  ///
  Stream<BlocState> get state => _stateController.stream;

  ///
  /// External processing of the event
  ///
  Stream<BlocState> eventHandler(BlocEvent event, BlocState currentState);

  ///
  /// initialState
  ///
  final BlocState initialState;

  //
  // Constructor
  //
  BlocEventStateBase({
    @required this.initialState,
  }){
    //
    // For each received event, we invoke the [eventHandler] and
    // emit any resulting newState
    //
    _eventController.listen((BlocEvent event){
      BlocState currentState = _stateController.value ?? initialState;
      eventHandler(event, currentState).forEach((BlocState newState){
        _stateController.sink.add(newState);
      });
    });
  }

  @override
  void dispose() {
    _eventController.close();
    _stateController.close();
  }
}

Как видно из кода, то, что мы определяем, на самом деле являетсяабстрактный класс, его необходимо расширить и внедрить, и основное внимание при реализации должно быть уделено определениюeventHandlerСпецифическое поведение этого метода.

Конечно, мы также можем видеть:

  • Sink (emitEvent в коде)МероприятиеEventВходной патрубок
  • Stream (указать в коде)Следите за статусом, который был выданState(s)Экспорт вывода как статус

Когда этот класс инициализируется(Обратитесь к разделу конструктора в коде):

  • Необходимо указать начальное состояниеinitialState
  • создалStreamSubscriptionсобытие для прослушивания ввода(Events)а также:
    • Назначение событий обработчикам событийeventHandler
    • бросить результатstate(s)

3.2 Расширенная реализация BlocEventState

Приведенный ниже код шаблона основан на расширении.BlocEventStateBaseАбстрактный класс реализует конкретныйBlocEventStateДобрый:

bloc_event_state_template.dart

class TemplateEventStateBloc extends BlocEventStateBase<BlocEvent, BlocState> {
  TemplateEventStateBloc()
      : super(
          initialState: BlocState.notInitialized(),
        );

  @override
  Stream<BlocState> eventHandler( BlocEvent event, BlocState currentState) async* {
     yield BlocState.notInitialized();
  }
}

Код шаблона выдаст ошибку, не волнуйтесь, это нормально... потому что мы еще не определили егоBlocState.notInitialized()... будет дано позже.

Этот шаблон просто дает начальное состояние при инициализацииinitialState, и перезаписываетeventHandlerметод.

Отметим также, что мы использовалиасинхронный генераторграмматика:async*а такжеyield

использоватьasync*модификатор, чтобы пометить метод какасинхронный генераторМетоды, такие как приведенный выше код, в каждом вызовеeventHandlerв рамках методаyieldзаявление, оно поставитyieldРезультат следующего выражения добавляется к выходуStreamсередина.

Если нам нужно инициировать серию действий через сериюStates(как вы увидите позже в примере), это особенно полезно.

Связанныйасинхронный генераторДля получения других сведений см.эта статья.

3.3. BlocEvent и BlocState

Как вы могли заметить, мы также определяемBlocEventа такжеBlocStateДва абстрактных класса, которые являются двумя абстрактными классами в соответствии с реальной ситуацией, то есть в соответствии с событием, которое вы хотите вызвать, и которое вызывается в реальных бизнес-сценариях для конкретныхРасширенная реализацияиз.

3.4 Компонент BlocEventStateBuilder

Последняя часть этого шаблонаBlocEventStateBuilderкомпонент, этот компонент может бытьBlocEventStateброшенныйState(s)Сделайте ответ на уровне просмотра.

код показывает, как показано ниже:

bloc_event_state_builder.dart

typedef Widget AsyncBlocEventStateBuilder<BlocState>(BuildContext context, BlocState state);

class BlocEventStateBuilder<BlocEvent,BlocState> extends StatelessWidget {
  const BlocEventStateBuilder({
    Key key,
    @required this.builder,
    @required this.bloc,
  }): assert(builder != null),
      assert(bloc != null),
      super(key: key);

  final BlocEventStateBase<BlocEvent,BlocState> bloc;
  final AsyncBlocEventStateBuilder<BlocState> builder;

  @override
  Widget build(BuildContext context){
    return StreamBuilder<BlocState>(
      stream: bloc.state,
      initialData: bloc.initialState,
      builder: (BuildContext context, AsyncSnapshot<BlocState> snapshot){
        return builder(context, snapshot.data);
      },
    );
  }
}

На самом деле этот компонент кроме одногоStreamBuilderНичего особенного снаружи, этоStreamBuilderЭффект заключается в том, что всякий раз, когда появляется новыйBlocStateПри броске вызовите его как новое значение параметраbuilderметод.


Хорошо, это все, что есть в этом шаблоне, давайте посмотрим, что мы можем с ними сделать...

3.5 Пример 1 управления событиями и состояниями: инициализация приложения(Application Initialization)

Первый пример демонстрирует ситуацию, когда приложение выполняет некоторые задачи при запуске.

Распространенным сценарием является заставка игры, также известная какSplashинтерфейс (независимо от того, анимирован он или нет), прежде чем отобразить реальный основной интерфейс игры, игровое приложение загрузит некоторые файлы с сервера, проверит, нужно ли его обновлять, попытается связаться с системным «игровым центром» и т. д.; и перед завершением инициализации. Чтобы у пользователей не возникало ощущения, что приложение ничего не делает, оно может также отображать индикатор выполнения, переключаться на отображение некоторых картинок через равные промежутки времени и так далее.

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

Первое, что нужно сделать, это определить события и статус...

3.5.1. Определение события: ApplicationInitializationEvent

В качестве примера здесь я рассматриваю только 2 события:

  • start: запуск процесса инициализации
  • stop: Используется для принудительной остановки процесса инициализации

Они определяются следующим образом:

app_init_event.dar

class ApplicationInitializationEvent extends BlocEvent {
  
  final ApplicationInitializationEventType type;

  ApplicationInitializationEvent({
    this.type: ApplicationInitializationEventType.start,
  }) : assert(type != null);
}

enum ApplicationInitializationEventType {
  start,
  stop,
}

3.5.2. Определение состояния: ApplicationInitializationState

ApplicationInitializationStateКласс предоставит информацию, связанную с процессом инициализации.

Также в качестве примера здесь я рассматриваю только:

  • 2 флага:
    • isInitializedИспользуется, чтобы указать, завершена ли инициализация
    • isInitializingИспользуется, чтобы знать, находимся ли мы в процессе инициализации
  • Стоимость завершения расписанияprograss

код показывает, как показано ниже:

app_init_state.dart

class ApplicationInitializationState extends BlocState {
  ApplicationInitializationState({
    @required this.isInitialized,
    this.isInitializing: false,
    this.progress: 0,
  });

  final bool isInitialized;
  final bool isInitializing;
  final int progress;

  factory ApplicationInitializationState.notInitialized() {
    return ApplicationInitializationState(
      isInitialized: false,
    );
  }

  factory ApplicationInitializationState.progressing(int progress) {
    return ApplicationInitializationState(
      isInitialized: progress == 100,
      isInitializing: true,
      progress: progress,
    );
  }

  factory ApplicationInitializationState.initialized() {
    return ApplicationInitializationState(
      isInitialized: true,
      progress: 100,
    );
  }
}

3.5.3 Реализация BLoC: ApplicationInitializationBloc

BLoCКонкретный процесс инициализации будет обработан на основе типа события.

код показывает, как показано ниже:

bloc_init_bloc.dart

class ApplicationInitializationBloc
    extends BlocEventStateBase<ApplicationInitializationEvent, ApplicationInitializationState> {
  ApplicationInitializationBloc()
      : super(
          initialState: ApplicationInitializationState.notInitialized(),
        );

  @override
  Stream<ApplicationInitializationState> eventHandler(
      ApplicationInitializationEvent event, ApplicationInitializationState currentState) async* {
    
    if (!currentState.isInitialized){
      yield ApplicationInitializationState.notInitialized();
    }

    if (event.type == ApplicationInitializationEventType.start) {
      for (int progress = 0; progress < 101; progress += 10){
        await Future.delayed(const Duration(milliseconds: 300));
        yield ApplicationInitializationState.progressing(progress);
      }
    }

    if (event.type == ApplicationInitializationEventType.stop){
      yield ApplicationInitializationState.initialized();
    }
  }
}

проиллюстрировать:

  • когда получилApplicationInitializationEventType.startсобытие, скорость завершения прогрессаprograssОт0прибыть100Начните считать (каждый шаг10), и еще нет100будет проходить каждый разyieldбросить новое состояние(state)Сообщает, что идет инициализация(isInitializing = true)И график завершенияprograssконкретное значение
  • когда получилApplicationInitializationEventType.stopсобытие, инициализация считается завершенной.
  • Как видите, я добавил небольшую задержку во время цикла.(delay), с целью демонстрацииFutureПрименимые сценарии (например, получение данных с сервера)

3.5.4. В комбинации

Теперь все, что осталось, это отобразить счетчик, представляющий скорость завершения выполнения, в значение false.SplashВ интерфейсе:

bloc_init_page.dart

class InitializationPage extends StatefulWidget {
  @override
  _InitializationPageState createState() => _InitializationPageState();
}

class _InitializationPageState extends State<InitializationPage> {
  ApplicationInitializationBloc bloc;

  @override
  void initState(){
    super.initState();
    bloc = ApplicationInitializationBloc();
    bloc.emitEvent(ApplicationInitializationEvent());
  }

  @override
  void dispose(){
    bloc?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext pageContext) {
    return SafeArea(
      child: Scaffold(
        body: Container(
          child: Center(
            child: BlocEventStateBuilder<ApplicationInitializationEvent, ApplicationInitializationState>(
              bloc: bloc,
              builder: (BuildContext context, ApplicationInitializationState state){
                if (state.isInitialized){
                  //
                  // Once the initialization is complete, let's move to another page
                  //
                  WidgetsBinding.instance.addPostFrameCallback((_){
                    Navigator.of(context).pushReplacementNamed('/home');
                  });
                }
                return Text('Initialization in progress... ${state.progress}%');
              },
            ),
          ),
        ),
      ),
    );
  }
}

проиллюстрировать:

  • В приложенииApplicationInitializationBlocНе все компоненты должны использоваться, поэтому только одинStatefulWidgetинициализировал (создал)BLoC
  • выпущено напрямуюApplicationInitializationEventType.startСобытие для триггераeventHandler
  • каждый разApplicationInitializationStateброшено, текстовое содержимое будет обновлено
  • После завершения процесса инициализации перейдите (перенаправьте) наHomeинтерфейс

Советы

Так как невозможно сразу перейти кHomeинтерфейс, вbuilderметод, используяWidgetsBinding.instance.addPostFrameCallback()метод запроса Flutter на выполнение прыжка после рендеринга. Ссылаться наaddPostFrameCallback()


3.6 Управление событиями и состояниями Пример 2: Аутентификация пользователя (вход и выход)

В этом примере я рассмотрел следующие сценарии:

  • Отображается автоматически, если пользователь не вошел в системуАутентификация / регистрацияинтерфейс
  • После того, как пользователь отправит информацию для входа, отображается индикатор выполнения (обведен кружком), представляющий обрабатываемый цикл.
  • Как только пользователь успешно войдет в систему, он перейдет кHomeинтерфейс
  • Пользователь может выйти из любого места в приложении.
  • Если пользователь выйдет из системы, он автоматически перейдет кЛогин (Аутентификация)интерфейс

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

Следующая диаграмма поясняет поток сценария, который будет реализован:

BlocAuthentication

страница среднего прыжкаDecisionPageбудет нести ответственностьавтоматическийперенаправить пользователя наAuthenticationинтерфейс илиHomeИнтерфейс, который зависит от статуса входа пользователя. КонечноDecisionPageОн не отображается пользователю и не должен рассматриваться как реальная страница.

Также первое, что нужно сделать, это определить некоторые события и состояния...

В качестве примера я рассмотрел только 2 события:

  • login: это событие генерируется, когда пользователь успешно входит в систему.
  • logout: это событие генерируется, когда пользователь выходит из системы.

Они определяются следующим образом:

bloc_auth_event.dart

abstract class AuthenticationEvent extends BlocEvent {
  final String name;

  AuthenticationEvent({
    this.name: '',
  });
}

class AuthenticationEventLogin extends AuthenticationEvent {
  AuthenticationEventLogin({
    String name,
  }) : super(
          name: name,
        );
}

class AuthenticationEventLogout extends AuthenticationEvent {}

3.6.2. Определение состояния: AuthenticationState

AuthenticationStateКласс предоставит информацию, связанную с процессом проверки.

Также в качестве примера я рассмотрел только:

  • 3 флага:
    • isAuthenticatedИспользуется для определения того, завершена ли проверка
    • isAuthenticatingИспользуется, чтобы узнать, идет ли процесс проверки
    • hasFailedИспользуется, чтобы указать, не прошла ли аутентификация
  • Аутентифицированное имя пользователя:name

код показывает, как показано ниже:

bloc_auth_state.dart

class AuthenticationState extends BlocState {
  AuthenticationState({
    @required this.isAuthenticated,
    this.isAuthenticating: false,
    this.hasFailed: false,
    this.name: '',
  });

  final bool isAuthenticated;
  final bool isAuthenticating;
  final bool hasFailed;

  final String name;
  
  factory AuthenticationState.notAuthenticated() {
    return AuthenticationState(
      isAuthenticated: false,
    );
  }

  factory AuthenticationState.authenticated(String name) {
    return AuthenticationState(
      isAuthenticated: true,
      name: name,
    );
  }

  factory AuthenticationState.authenticating() {
    return AuthenticationState(
      isAuthenticated: false,
      isAuthenticating: true,
    );
  }

  factory AuthenticationState.failure() {
    return AuthenticationState(
      isAuthenticated: false,
      hasFailed: true,
    );
  }
}

3.6.3 Реализация BLoC: AuthenticationBloc

BLoCКонкретный процесс аутентификации будет обрабатываться в зависимости от типа события.

код показывает, как показано ниже:

bloc_auth_bloc.dart

class AuthenticationBloc
    extends BlocEventStateBase<AuthenticationEvent, AuthenticationState> {
  AuthenticationBloc()
      : super(
          initialState: AuthenticationState.notAuthenticated(),
        );

  @override
  Stream<AuthenticationState> eventHandler(
      AuthenticationEvent event, AuthenticationState currentState) async* {

    if (event is AuthenticationEventLogin) {
      // Inform that we are proceeding with the authentication
      yield AuthenticationState.authenticating();

      // Simulate a call to the authentication server
      await Future.delayed(const Duration(seconds: 2));

      // Inform that we have successfuly authenticated, or not
      if (event.name == "failure"){
        yield AuthenticationState.failure();
      } else {
        yield AuthenticationState.authenticated(event.name);
      }
    }

    if (event is AuthenticationEventLogout){
      yield AuthenticationState.notAuthenticated();
    }
  }
}

проиллюстрировать:

  • когда получилAuthenticationEventLoginсобытие, пройдетyieldбросить новое состояние(state)Сообщите, что аутентификация выполняется(isAuthenticating = true)
  • После завершения аутентификации выдается другое новое состояние.(state)уведомление сделано
  • когда получилAuthenticationEventLogoutсобытие, выдается новое состояние(state)Сообщите пользователю, что он больше не находится в проверенном состоянии

3.6.4 Страница входа: AuthenticationPage

Как видите, эта страница не очень сложна для иллюстрации.

Код и описание следующие:

bloc_auth_page.dart

class AuthenticationPage extends StatelessWidget {
  ///
  /// Prevents the use of the "back" button
  ///
  Future<bool> _onWillPopScope() async {
    return false;
  }

  @override
  Widget build(BuildContext context) {
    AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
    return WillPopScope(
      onWillPop: _onWillPopScope,
      child: SafeArea(
        child: Scaffold(
          appBar: AppBar(
            title: Text('Authentication Page'),
            leading: Container(),
          ),
          body: Container(
            child:
                BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
              bloc: bloc,
              builder: (BuildContext context, AuthenticationState state) {
                if (state.isAuthenticating) {
                  return PendingAction();
                }

                if (state.isAuthenticated){
                  return Container();
                }
                
                List<Widget> children = <Widget>[];

                // Button to fake the authentication (success)
                children.add(
                  ListTile(
                      title: RaisedButton(
                        child: Text('Log in (success)'),
                        onPressed: () {
                            bloc.emitEvent(AuthenticationEventLogin(name: 'Didier'));
                        },
                      ),
                    ),
                );

                // Button to fake the authentication (failure)
                children.add(
                  ListTile(
                      title: RaisedButton(
                        child: Text('Log in (failure)'),
                        onPressed: () {
                            bloc.emitEvent(AuthenticationEventLogin(name: 'failure'));
                        },
                      ),
                    ),
                );

                // Display a text if the authentication failed
                if (state.hasFailed){
                  children.add(
                    Text('Authentication failure!'),
                  );
                }

                return Column(
                  children: children,
                );    
              },
            ),
          ),
        ),
      ),
    );
  }
}

проиллюстрировать:

  • Строка 11: Получить на страницеAuthenticationBloc
  • Строки 24 ~ 70: слушайте брошенныйAuthenticationState:
    • Если проверка выполняется, отобразите круговой индикатор выполнения (круг), чтобы информировать пользователя о том, что обработка выполняется, и запретить пользователю доступ к другим страницам (строки 25 ~ 27).
    • Если проверка прошла успешно, отобразите пустойContainer, т.е. ничего не отображается (строки 29~31)
    • Если пользователь не вошел в систему, отобразите 2 кнопки для имитации успешного и неудачного входа в систему.
    • При нажатии на одну из кнопок излучаетсяAuthenticationEventLoginСобытия и некоторые параметры (обычно используемые для обработки проверки)
    • Если аутентификация не удалась, отобразить сообщение об ошибке (строки 60~64)

Хорошо, ничего больше, это легко, верно?

Советы

Вы, должно быть, заметили, я завернул страницу вWillPopScopeВнутри это связано с тем, что аутентификация является необходимым шагом, если вход в систему не будет успешным (аутентификация пройдена), я не хочу, чтобы пользователь использовал клавишу «Назад», предоставленную устройством Android, чтобы пропустить аутентификацию для доступа к другим страницам.

3.6.5. Промежуточная страница перехода: DecisionPage

Как упоминалось ранее, я хочу, чтобы приложение автоматически переходило кAuthenticationPageилиHomePage

Код и описание следующие:

bloc_decision_page.dart

class DecisionPage extends StatefulWidget {
  @override
  DecisionPageState createState() {
    return new DecisionPageState();
  }
}

class DecisionPageState extends State<DecisionPage> {
  AuthenticationState oldAuthenticationState;

  @override
  Widget build(BuildContext context) {
    AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
    return BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
      bloc: bloc,
      builder: (BuildContext context, AuthenticationState state) {
        if (state != oldAuthenticationState){
          oldAuthenticationState = state;

          if (state.isAuthenticated){
            _redirectToPage(context, HomePage());
          } else if (state.isAuthenticating || state.hasFailed){
  //do nothing
          } else {
            _redirectToPage(context, AuthenticationPage());
          }
        }
        // This page does not need to display anything since it will
        // always remind behind any active page (and thus 'hidden').
        return Container();
      }
    );
  }

  void _redirectToPage(BuildContext context, Widget page){
    WidgetsBinding.instance.addPostFrameCallback((_){
      MaterialPageRoute newRoute = MaterialPageRoute(
          builder: (BuildContext context) => page
        );

      Navigator.of(context).pushAndRemoveUntil(newRoute, ModalRoute.withName('/decision'));
    });
  }
}

намекать

Чтобы подробно объяснить следующие проблемы, давайте сначала вернемся к обработке Flutter.Pages(то есть,Маршрут), то есть с помощьюNavigatorобъект для управленияRoutes,а такжеNavigatorОбъекты, созданныеOverlayОбъекты; этоOverlayНа самом деле он содержит несколькоOverlayEntryизStackобъект, и каждыйOverlayEntryОн содержитPage;

когда мы проходимNavigator.of(context)Также обновляется при манипулировании стеком маршрутизации для отправки, извлечения или замены.Overlayобъект (то естьStackобъект), другими словами, эти операции приводят кStackрефакторинг объектов;StackПри рефакторингеOverlayEntry(включая его содержаниеPage) Последует реконструкция;

Результат:

когда мы проходимNavigator.of(context)После маршрутизации все остальные страницы подвергаются рефакторингу!

  • Итак, почему я должен реализовать это какStatefulWidget ?

    Чтобы иметь возможность ответитьAuthenticationStateлюбые изменения в этомpageЕго необходимо сохранять на протяжении всего жизненного цикла приложения;

    И согласно приведенным выше советам, каждый раз, когда вы звонитеNavigator.of(context), эта страница будет подвергнута рефакторингу, а значит, и рефакторингу подвергнетсяBlocEventStateBuilder,без сомненийBlocEventStateBuilderвнутриbuilderтакже будет вызван метод;

    потому что этоbuilderметод отвечает за перенаправление пользователя наAuthenticationStateСоответствующая страница, через которую должно пройти перенаправлениеNavigator.of(context)Для достижения ... Очевидно, что бесконечная петля

    Так вот чтобы этого не произошло, нам нужно поставить "последний"AuthenticationStateсохранить только когда новыйAuthenticationStateДа и сэкономленное время уже не то, мы просто перенаправляем обработку;

    А для достижения хранения нужно использоватьStatefulWidgetхарактеристики, будет "последним"AuthenticationStateположи этоStateизoldAuthenticationStateв свойствах.

  • Как именно это работает?

    Как упоминалось выше, всякий раз, когдаAuthenticationStateпри броске,BlocEventStateBuilderпозвонюbuilderметод, согласноisAuthenticatedОпределите, мы знаем, на какую страницу ссылается пользователь, на которую он перенаправляется.

Советы

из-заbuilderне может напрямую перейти к другим интерфейсам, мы использовалиWidgetsBinding.instance.addPostFrameCallback()метод запроса Flutter на выполнение прыжка после рендеринга.

Кроме того, кромеDecisionPageВ дополнение к сохранению всего жизненного цикла приложения нам необходимо удалить все другие существующие страницы в стеке маршрутизации перед перенаправлением, поэтому мы используемnavigator.of (контекст) .push и удалить до (...)для достижения этой цели. Ссылаться наpushAndRemoveUntil()


3.6.6 Выход пользователя

Чтобы разрешить пользователям выходить из системы, создайтеLogOutButton, в любом месте приложения.

Эту кнопку нужно только нажать, чтобы испуститьAuthenticationEventLogout()событие, которое запускает следующие действия автоматической обработки:

  1. По событиюAuthenticationBlocобрабатывать
  2. БросаетAuthentiationState(isAuthenticated = false)
  3. Брошенный статус будет присвоенDecisionPageпройти черезBlocEventStateBuilderобрабатывать
  4. Наконец, перенаправьте пользователя наAuthenticationPage

Код кнопки следующий:

bloc_log_out_button.dart

class LogOutButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
    return IconButton(
      icon: Icon(Icons.exit_to_app),
      onPressed: () {
        bloc.emitEvent(AuthenticationEventLogout());
      },
    );
  }
}

3.6.7. Инъекция аутентификацииBLOC

из-за необходимостиAuthenticationBlocДоступен с любой страницы приложения, поэтому мы внедряем его какРодитель MaterialApp,Следующим образом:

bloc_auth_app.dart

void main() => runApp(Application());

class Application extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<AuthenticationBloc>(
      bloc: AuthenticationBloc(),
      child: MaterialApp(
        title: 'BLoC Samples',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: DecisionPage(),
      ),
    );
  }
}

4. Проверка формы

BLoCЕще одним интересным сценарием применения является проверка формы, например:

  • проверитьTextFieldУдовлетворяет ли элемент формы некоторым бизнес-правилам
  • Отображение сообщения при возникновении ошибки проверки бизнес-правила
  • Автоматически обрабатывать доступность компонентов формы на основе бизнес-правил

В приведенном ниже примере я используюRegistrationFormформа, эта форма содержит 3TextField(соответственно e-mailemail,парольpasswordи повторите парольconfirmPassword) и кнопкаRaisedButtonИспользуется для инициации обработки регистрации

Бизнес-правила, которые вы хотите реализовать:

  • emailЭто должен быть действующий адрес электронной почты, в противном случае будет отображаться сообщение об ошибке.
  • passwordОн также должен быть действительным, то есть включать как минимум 1 заглавную букву, 1 строчную букву, 1 цифру и 1 специальный символ и не менее 8 символов, в противном случае необходимо отобразить сообщение об ошибке.
  • Повторите парольretype passwordПомимо необходимости иpasswordВ дополнение к тем же правилам проверки вам также необходимоpasswordТочно так же, если нет, вывести сообщение об ошибке
  • register

4.1. RegistrationFormBloc

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

bloc_reg_form_bloc.dart

class RegistrationFormBloc extends Object with EmailValidator, PasswordValidator implements BlocBase {

  final BehaviorSubject<String> _emailController = BehaviorSubject<String>();
  final BehaviorSubject<String> _passwordController = BehaviorSubject<String>();
  final BehaviorSubject<String> _passwordConfirmController = BehaviorSubject<String>();

  //
  //  Inputs
  //
  Function(String) get onEmailChanged => _emailController.sink.add;
  Function(String) get onPasswordChanged => _passwordController.sink.add;
  Function(String) get onRetypePasswordChanged => _passwordConfirmController.sink.add;

  //
  // Validators
  //
  Stream<String> get email => _emailController.stream.transform(validateEmail);
  Stream<String> get password => _passwordController.stream.transform(validatePassword);
  Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
    .doOnData((String c){
      // If the password is accepted (after validation of the rules)
      // we need to ensure both password and retyped password match
      if (0 != _passwordController.value.compareTo(c)){
        // If they do not match, add an error
        _passwordConfirmController.addError("No Match");
      }
    });

  //
  // Registration button
  Stream<bool> get registerValid => Observable.combineLatest3(
                                      email, 
                                      password, 
                                      confirmPassword, 
                                      (e, p, c) => true
                                    );

  @override
  void dispose() {
    _emailController?.close();
    _passwordController?.close();
    _passwordConfirmController?.close();
  }
}

проиллюстрировать:

  • В этом классе мы сначала инициализировали 3BehaviorSubject, чтобы обработать 3 в формеTextFieldизStream
  • Этот класс предоставляет 3Function(String), используется для полученияTextFieldввод
  • Этот класс предоставляет 3Stream<String>,существуетTextFieldЕсли проверка не удалась, отобразите соответствующее сообщение об ошибке.
  • Также предоставляет 1Stream<bool>Роль основана на результатах проверки всех форм, контроляRaisedButtonэто полезно или нет(enable/disabe)

Хорошо, давайте углубимся в некоторые подробности...

Вы могли заметить, что этоBLoCКод для класса немного особенный, например:

class RegistrationFormBloc extends Object 
                           with EmailValidator, PasswordValidator 
                           implements BlocBase {
  ...
}

использовалwithКлючевое слово указывает, что этот класс используетMIXINS(способ повторного использования кода класса в другом классе), и для того, чтобы использоватьwith, этот класс также должен быть основан наObjectкласс для расширения. Эти примеси содержат соответствующие методы проверки электронной почты и пароля.

Рекомендуемая литература для получения дополнительной информации о миксинахЭта статья Ромена Растеля.

4.1.1 Миксины для проверки формы

Я здесь толькоEmailValidatorСделайте описание, потому чтоPasswordValidatorТоже похоже.

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

bloc_email_validator.dart

const String _kEmailRule = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";


class EmailValidator {
  final StreamTransformer<String,String> validateEmail = 
      StreamTransformer<String,String>.fromHandlers(handleData: (email, sink){
        final RegExp emailExp = new RegExp(_kEmailRule);


        if (!emailExp.hasMatch(email) || email.isEmpty){
          sink.addError('Entre a valid email');
        } else {
          sink.add(email);
        }
      });
}

Этот класс предоставляетfinalметод(validateEmail), этот метод фактически возвращаетStreamTransformerПример

намекать

StreamTransformerназывается так:stream.transform(StreamTransformer)

StreamTransformerотStreamПолучить вход, а затем цитироватьStreamизtransformметод для обработки ввода и повторного ввода обработанных данных в исходныйStreamсередина.

В вышеуказанном коде, в соответствии с процессом, содержащимрегулярное выражениеПроверьте ввод и повторно введите ввод, если он совпадаетstreamin; если не совпадает, ввести сообщение об ошибке вstream

4.1.2 Зачем использоватьstream.transform()?

Как упоминалось ранее, если проверка прошла успешно,StreamTransformerповторно введет ввод обратно вStreamКак ты работаешь?

Observable.combineLatest3()этот метод, он есть в каждомStreamпрежде чем все выкинуть хотя бы одно значение, иНе будетдать любое значение

Как показано ниже:

Observable.combineLatest3

  • Если пользователь вводитemailЭто эффективно,emailизstreamвыкинет содержимое пользовательского ввода, и в то же времяObservable.combineLatest3()ввод
  • Если пользователь вводитemailэто недействительно,emailизstreamСообщение об ошибке будет добавлено к (иstreamне кидает данные)
  • passwordа такжеretype passwordаналогичный механизм
  • Когда все 3 из них проверены (т.е. 3streamоба бросают данные),Observable.combineLatest3()буду использовать(e, p, c) => trueметод выдаетtrueзначение (см. строку кода 35)

4.1.3 Аутентификация по паролю и дубликату пароля

Я видел много проблем с проверкой паролей и повторяющихся паролей в Интернете.Решений должно быть много.Здесь я объясню два из них.

4.1.3.1. Нет ошибки в базовом случае

Код для первого решения выглядит следующим образом:

bloc_password_valid_1.dart

Stream<bool> get registerValid => Observable.combineLatest3(
                                      email, 
                                      password, 
                                      confirmPassword, 
                                      (e, p, c) => (0 == p.compareTo(c))
                                    );

Это решение просто после проверки двух паролей, сравните их и, если они одинаковы, бросьтеtrueстоимость.

И поэтому мы увидим дальше,RegisterregisterValid streamДа, если два пароля разные,registerValid streamне будет выдавать никакого значения, поэтомуRegisterКнопка по-прежнему недоступна.

Однако пользователь не получает никаких сообщений об ошибках, поэтому он не понимает, что происходит.

4.1.3.2 Сценарии с сообщениями об ошибках

Другой вариант - поставитьconfirmPassword streamМетод обработки был расширен, и код выглядит следующим образом:

bloc_password_valid_2.dart

Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
    .doOnData((String c){
      // If the password is accepted (after validation of the rules)
      // we need to ensure both password and retyped password match
      if (0 != _passwordController.value.compareTo(c)){
        // If they do not match, add an error
        _passwordConfirmController.addError("No Match");
      }
    });

однаждыretype passwordПроверка бизнес-правила пройдена, и содержимое, введенное пользователем, будетStreamбросай и звониdoOnData()метод, при котором_passwordController.value.compareTo()Получить ли сpassword streamДанные совпадают, если нет, мы можем добавить сообщение об ошибке.


4.2. Компонент RegistrationForm

Прежде чем объяснять объяснение, давайте посмотримFormКод реализации компонента:

bloc_reg_form.dart

class RegistrationForm extends StatefulWidget {
  @override
  _RegistrationFormState createState() => _RegistrationFormState();
}


class _RegistrationFormState extends State<RegistrationForm> {
  RegistrationFormBloc _registrationFormBloc;


  @override
  void initState() {
    super.initState();
    _registrationFormBloc = RegistrationFormBloc();
  }


  @override
  void dispose() {
    _registrationFormBloc?.dispose();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return Form(
      child: Column(
        children: <Widget>[
          StreamBuilder<String>(
              stream: _registrationFormBloc.email,
              builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                return TextField(
                  decoration: InputDecoration(
                    labelText: 'email',
                    errorText: snapshot.error,
                  ),
                  onChanged: _registrationFormBloc.onEmailChanged,
                  keyboardType: TextInputType.emailAddress,
                );
              }),
          StreamBuilder<String>(
              stream: _registrationFormBloc.password,
              builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                return TextField(
                  decoration: InputDecoration(
                    labelText: 'password',
                    errorText: snapshot.error,
                  ),
                  obscureText: false,
                  onChanged: _registrationFormBloc.onPasswordChanged,
                );
              }),
          StreamBuilder<String>(
              stream: _registrationFormBloc.confirmPassword,
              builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
                return TextField(
                  decoration: InputDecoration(
                    labelText: 'retype password',
                    errorText: snapshot.error,
                  ),
                  obscureText: false,
                  onChanged: _registrationFormBloc.onRetypePasswordChanged,
                );
              }),
          StreamBuilder<bool>(
              stream: _registrationFormBloc.registerValid,
              builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
                return RaisedButton(
                  child: Text('Register'),
                  onPressed: (snapshot.hasData && snapshot.data == true)
                      ? () {
                          // launch the registration process
                        }
                      : null,
                );
              }),
        ],
      ),
    );
  }
}

проиллюстрировать:

  • потому чтоRegisterFormBlocОн используется только для обработки проверки формы, поэтому целесообразно инициализировать (создавать экземпляр) только в компоненте формы.
  • каждыйTextFieldОни включены вStreamBuilder<String>чтобы иметь возможность реагировать на любые результаты процесса проверки (см.errorText: снимок.ошибка)
  • каждый разTextFieldКогда содержимое, введенное в, изменяется, мы все передаем введенное содержимое черезonChanged:_registrationFormBloc.onEmailChanged(войтиemailслучай) отправлен наBLoCаутентификация,
  • RegisterButtonТакже включены вStreamBuilder<bool>середина
    • если_registrationFormBloc.registerValidзначение выбрасывается,onPressedВыброшенное значение будет обработано после того, как пользователь нажмет
    • Если значение не выбрасывается,onPressedметод указан какnull, кнопка будет отключена

Отлично! Вы можете видеть, что в компоненте формы вы не видите никакого кода, относящегося к бизнес-правилам, а это означает, что мы можем изменять бизнес-правила по своему желанию без каких-либо изменений самого компонента формы, что превосходно!


5. Часть режима

Иногда компоненту необходимо управлять своим собственным поведением в соответствии с его окружением (принадлежит ли он к списку/коллекции/компоненту и т. д.) В качестве последнего примера в этой статье мы рассмотрим следующие сценарии:

  • Приложение предоставляет несколько продуктов(item)
  • Пользователь может положить выбранные товары в корзину
  • Каждый товар можно положить в корзину только один раз
  • Товары в корзине можно удалять
  • Удаленные товары могут быть возвращены пользователем в корзину.

В примере для каждого товара отображается кнопка, которая определяет его поведение в зависимости от того, находится ли товар в корзине:

  • Если товар находится в корзине, разрешите пользователю щелкнуть, чтобы удалить товар из корзины.
  • Если это не в корзине для покупок, соответствующий товар будет добавлен в корзину для покупок после кликов пользователя

Чтобы лучше проиллюстрироватьPart ofрежиме я использовал следующую структуру кода:

  • реализоватьShopping Page, чтобы отобразить список всех возможных продуктов
  • Shopping PageКаждый элемент будет иметь кнопку, которая добавляет или удаляет элемент из корзины, в зависимости от того, находится ли элемент уже в корзине.
  • еслиShopping Pageдобавлен в корзину, кнопка будетавтоматическийОбновление, позволяющее пользователю щелкнуть еще раз, чтобы удалить элемент из корзины (и наоборот); рефакторинг для этого процесса не требуется.Shopping Page
  • создать другую страницуShopping Basket, чтобы отобразить все товары, которые были добавлены в корзину
  • ОтShopping BasketУдалите со страницы все товары, которые были добавлены в корзину.

Уведомление

"Часть режима«Это мое собственное имя, а не официальное.

5.1. ShoppingBloc

Как вы, возможно, уже подумали, нам нужно рассмотреть возможность сдачиBLoCдля обработки списка всех элементов иShopping BasketСписок товаров (добавленных в корзину) на странице

этоBLoCкод показывает, как показано ниже:

bloc_shopping_bloc.dart

class ShoppingBloc implements BlocBase {
  // List of all items, part of the shopping basket
  Set<ShoppingItem> _shoppingBasket = Set<ShoppingItem>();


  // Stream to list of all possible items
  BehaviorSubject<List<ShoppingItem>> _itemsController = BehaviorSubject<List<ShoppingItem>>();
  Stream<List<ShoppingItem>> get items => _itemsController;


  // Stream to list the items part of the shopping basket
  BehaviorSubject<List<ShoppingItem>> _shoppingBasketController = BehaviorSubject<List<ShoppingItem>>(seedValue: <ShoppingItem>[]);
  Stream<List<ShoppingItem>> get shoppingBasket => _shoppingBasketController;


  @override
  void dispose() {
    _itemsController?.close();
    _shoppingBasketController?.close();
  }


  // Constructor
  ShoppingBloc() {
    _loadShoppingItems();
  }


  void addToShoppingBasket(ShoppingItem item){
    _shoppingBasket.add(item);
    _postActionOnBasket();
  }


  void removeFromShoppingBasket(ShoppingItem item){
    _shoppingBasket.remove(item);
    _postActionOnBasket();
  }


  void _postActionOnBasket(){
    // Feed the shopping basket stream with the new content
    _shoppingBasketController.sink.add(_shoppingBasket.toList());
    
    // any additional processing such as
    // computation of the total price of the basket
    // number of items, part of the basket...
  }


  //
  // Generates a series of Shopping Items
  // Normally this should come from a call to the server
  // but for this sample, we simply simulate
  //
  void _loadShoppingItems() {
    _itemsController.sink.add(List<ShoppingItem>.generate(50, (int index) {
      return ShoppingItem(
        id: index,
        title: "Item $index",
        price: ((Random().nextDouble() * 40.0 + 10.0) * 100.0).roundToDouble() /
            100.0,
        color: Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0)
            .withOpacity(1.0),
      );
    }));
  }
}

Наверное, единственное, что нуждается в объяснении, это_postActionOnBasket()Подход: каждый раз, когда мы добавляем или удаляем товар в корзину, нам нужно «обновить»_shoppingBasketControllerконтрольstreamсодержание, следите заstreamкомпоненты уведомляются об изменении, чтобы сам компонент можно было обновить или перестроить(refresh/rebuild)

5.2. ShoppingPage

Эта страница очень проста, на ней просто показаны все продукты:

bloc_shopping_page.dart

class ShoppingPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ShoppingBloc bloc = BlocProvider.of<ShoppingBloc>(context);


    return SafeArea(
        child: Scaffold(
      appBar: AppBar(
        title: Text('Shopping Page'),
        actions: <Widget>[
          ShoppingBasket(),
        ],
      ),
      body: Container(
        child: StreamBuilder<List<ShoppingItem>>(
          stream: bloc.items,
          builder: (BuildContext context,
              AsyncSnapshot<List<ShoppingItem>> snapshot) {
            if (!snapshot.hasData) {
              return Container();
            }
            return GridView.builder(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                childAspectRatio: 1.0,
              ),
              itemCount: snapshot.data.length,
              itemBuilder: (BuildContext context, int index) {
                return ShoppingItemWidget(
                  shoppingItem: snapshot.data[index],
                );
              },
            );
          },
        ),
      ),
    ));
  }
}

проиллюстрировать:

  • AppBarКнопка отображается:
    • Отображает количество товаров в корзине
    • При нажатии перейти кShoppingBasketстраница
  • список используемых продуктовGridViewмакет, этоGridViewсодержится в *StreamBuilder>*
  • По одному на каждый продуктShoppingItemWidget

5.3. ShoppingBasketPage

This page is very similar to the ShoppingPage except that the StreamBuilder is now listening to variations of the _shoppingBasket stream, exposed by the ShoppingBloc.

эта страница иShoppingPageочень похоже, за исключением того, чтоStreamBuilderОбъект прослушивания — этоShoppingBlocкоторый предоставил_shoppingBasket streamрезультат изменения


5.4. ShoppingItemWidget и ShoppingItemBloc

Part Ofрежим зависит отShoppingItemWidgetа такжеShoppingItemBlocКомбинированное применение двух элементов:

  • ShoppingItemWidgetОтвечает за отображение:
    • Информация о товаре
    • Добавить в корзину или удалить кнопки
  • ShoppingItemBlocответственный за сообщениеShoppingItemWidgetСтатус "в корзине"

Давайте посмотрим, как они все работают вместе...

5.4.1. ShoppingItemBloc

ShoppingItemBlocкаждымShoppingItemWidgetдля создания экземпляра и предоставления ему собственного идентификатора элемента(identity);

BLoCбудет контролироватьShoppingBasket stream, и проверьте, есть ли уже товар с указанным ID в корзине;

Если у вас есть в вашей корзине,BLoCtrue), соответствующий идентификаторуShoppingItemWidgetЭто логическое значение будет зафиксировано, чтобы знать, что оно уже находится в корзине покупок.

СледующееBLoCкод:

bloc_shopping_item_bloc.dart

class ShoppingItemBloc implements BlocBase {
  // Stream to notify if the ShoppingItemWidget is part of the shopping basket
  BehaviorSubject<bool> _isInShoppingBasketController = BehaviorSubject<bool>();
  Stream<bool> get isInShoppingBasket => _isInShoppingBasketController;


  // Stream that receives the list of all items, part of the shopping basket
  PublishSubject<List<ShoppingItem>> _shoppingBasketController = PublishSubject<List<ShoppingItem>>();
  Function(List<ShoppingItem>) get shoppingBasket => _shoppingBasketController.sink.add;


  // Constructor with the 'identity' of the shoppingItem
  ShoppingItemBloc(ShoppingItem shoppingItem){
    // Each time a variation of the content of the shopping basket
    _shoppingBasketController.stream
                          // we check if this shoppingItem is part of the shopping basket
                         .map((list) => list.any((ShoppingItem item) => item.id == shoppingItem.id))
                          // if it is part
                         .listen((isInShoppingBasket)
                              // we notify the ShoppingItemWidget 
                            => _isInShoppingBasketController.add(isInShoppingBasket));
  }


  @override
  void dispose() {
    _isInShoppingBasketController?.close();
    _shoppingBasketController?.close();
  }
}

5.4.2. ShoppingItemWidget

Этот компонент отвечает за:

  • СоздаватьShoppingItemBlocПример и пройти удостоверение личностиBLoCПример
  • следить за любымShoppingBasketизменения содержимого и сообщать об измененияхBLoC
  • мониторShoppingItemBlocЗнайте свой собственный статус «в корзине покупок»
  • Отображение соответствующей кнопки (добавить/удалить) в зависимости от того, находится ли она в корзине покупок
  • Пользователь нажимает кнопку и дает ответ:
    • Добавляет себя в корзину, когда пользователь нажимает кнопку «Добавить».
    • Удаляет себя из корзины, когда пользователь нажимает кнопку "Удалить"

Давайте взглянем на конкретный код реализации и инструкции:

bloc_shopping_item.dart

class ShoppingItemWidget extends StatefulWidget {
  ShoppingItemWidget({
    Key key,
    @required this.shoppingItem,
  }) : super(key: key);


  final ShoppingItem shoppingItem;


  @override
  _ShoppingItemWidgetState createState() => _ShoppingItemWidgetState();
}


class _ShoppingItemWidgetState extends State<ShoppingItemWidget> {
  StreamSubscription _subscription;
  ShoppingItemBloc _bloc;
  ShoppingBloc _shoppingBloc;


  @override
  void didChangeDependencies() {
    super.didChangeDependencies();


    // As the context should not be used in the "initState()" method,
    // prefer using the "didChangeDependencies()" when you need
    // to refer to the context at initialization time
    _initBloc();
  }


  @override
  void didUpdateWidget(ShoppingItemWidget oldWidget) {
    super.didUpdateWidget(oldWidget);


    // as Flutter might decide to reorganize the Widgets tree
    // it is preferable to recreate the links
    _disposeBloc();
    _initBloc();
  }


  @override
  void dispose() {
    _disposeBloc();
    super.dispose();
  }


  // This routine is reponsible for creating the links
  void _initBloc() {
    // Create an instance of the ShoppingItemBloc
    _bloc = ShoppingItemBloc(widget.shoppingItem);


    // Retrieve the BLoC that handles the Shopping Basket content 
    _shoppingBloc = BlocProvider.of<ShoppingBloc>(context);


    // Simple pipe that transfers the content of the shopping
    // basket to the ShoppingItemBloc
    _subscription = _shoppingBloc.shoppingBasket.listen(_bloc.shoppingBasket);
  }


  void _disposeBloc() {
    _subscription?.cancel();
    _bloc?.dispose();
  }


  Widget _buildButton() {
    return StreamBuilder<bool>(
      stream: _bloc.isInShoppingBasket,
      initialData: false,
      builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
        return snapshot.data
            ? _buildRemoveFromShoppingBasket()
            : _buildAddToShoppingBasket();
      },
    );
  }


  Widget _buildAddToShoppingBasket(){
    return RaisedButton(
      child: Text('Add...'),
      onPressed: (){
        _shoppingBloc.addToShoppingBasket(widget.shoppingItem);
      },
    );
  }


  Widget _buildRemoveFromShoppingBasket(){
    return RaisedButton(
      child: Text('Remove...'),
      onPressed: (){
        _shoppingBloc.removeFromShoppingBasket(widget.shoppingItem);
      },
    );
  }


  @override
  Widget build(BuildContext context) {
    return Card(
      child: GridTile(
        header: Center(
          child: Text(widget.shoppingItem.title),
        ),
        footer: Center(
          child: Text('${widget.shoppingItem.price} €'),
        ),
        child: Container(
          color: widget.shoppingItem.color,
          child: Center(
            child: _buildButton(),
          ),
        ),
      ),
    );
  }
}

5.5 Как именно это работает?

Конкретная работа каждой части может относиться к следующему рисунку

Part_Of


постскриптум

Очередная длинная статья, хотелось бы поменьше писать, но думаю многое нужно объяснить понятно.

Как я уже говорил в предисловии, лично я использовал эти «шаблоны» много в разработке, они сэкономили мне много времени и усилий, и полученный код легче читать и отладки; кроме того, он также помогает в развязке и разделение бизнеса и взгляда.

Определенно есть множество других способов сделать это, даже лучших способов, но шаблон из этой статьи действительно работает для меня, поэтому я хочу поделиться им с вами.

Следите за новыми статьями и удачного программирования.

--Конец полного текста--