Flutter - BLoC 第二講

本篇已同步到 個人博客 ,歡迎常來。

[譯文]Reactive Programming - Streams - BLoC實際用例 原文

BLoC,Reactive Programming,Streams - 實際用例和有用模式。

[TOC]
注:此處的"toc"應顯示為目錄,但是簡書不支持,顯示不出來。

介紹

在介紹了BLoC,Reactive Programming和Streams的概念后,我在一段時間之前做了一些介紹,盡管與我分享一些我經(jīng)常使用并且個人覺得非常有用的模式(至少對我而言)可能會很有趣。這些模式使我在開發(fā)過程中節(jié)省了大量時間,并使我的代碼更易于閱讀和調(diào)試。

我要談的話題是:

  • 1.BLoC Provider and InheritedWidget
  • 2.在哪里初始化BLoC?
  • 3.事件狀態(tài)(允許根據(jù)事件響應狀態(tài)轉(zhuǎn)換)
  • 4.表格驗證(允許根據(jù)條目和驗證控制表單的行為)
  • 5.Part Of(允許Widget根據(jù)其在列表中的存在來調(diào)整其行為)

完整的源代碼可以在GitHub上找到。

1.BLoC Provider and InheritedWidget

我借此文章的機會介紹我的BlocProvider的另一個版本,它現(xiàn)在依賴于一個InheritedWidget。

使用InheritedWidget的優(yōu)點是我們獲得了性能。

1.1. 之前的實現(xiàn)

我之前版本的BlocProvider實現(xiàn)為常規(guī)StatefulWidget,如下所示:

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分配的資源。

這很好用但從性能角度來看并不是最佳的。

context.ancestorWidgetOfExactType()是一個為時間復雜度為O(n)的函數(shù),為了檢索某種類型的祖先,它將對widget樹 做向上導航,從上下文開始,一次遞增一個父,直到完成。如果從上下文到祖先的距離很?。碠(n)結(jié)果很少),則可以接受對此函數(shù)的調(diào)用,否則應該避免。這是這個函數(shù)的代碼。

@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. 新的實現(xiàn)
新實現(xiàn)依賴于StatefulWidget,并結(jié)合InheritedWidget:

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;
}

優(yōu)點是這個解決方案是性能。

由于使用了InheritedWidget,它現(xiàn)在可以調(diào)用context.ancestorInheritedElementForWidgetOfExactType()函數(shù),它是一個O(1),這意味著祖先的檢索是立即的,如其源代碼所示:

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

這來自于所有InheritedWidgets都由Framework記憶的事實。

  • 為什么使用 ancestorInheritedElementForWidgetOfExactType ?
  • 您可能已經(jīng)注意到我使用 ancestorInheritedElementForWidgetOfExactType 方法而不是通常的 inheritFromWidgetOfExactType 。
  • 原因是我不希望上下文調(diào)用的BlocProvider被注冊為InheritedWidget的依賴項,因為我不需要它。

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?

要回答這個問題,您需要弄清楚其使用范圍。

2.1.應用程序中隨處可用

假設您必須處理與用戶身份驗證/配置文件,用戶首選項,購物籃相關(guān)的一些機制, 可從應用程序的任何可能部分(例如,從不同頁面)獲得獲得BLoC(),存在兩種方式使這個BLoC可訪問。

2.1.1.使用全局單例
import 'package:rxdart/rxdart.dart';

class GlobalBloc {
  ///
  /// 與此BLoC相關(guān)的流
  ///
  BehaviorSubject<String> _controller = BehaviorSubject<String>();
  Function(String) get push => _controller.sink.add;
  Stream<String> get stream => _controller;

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

GlobalBloc globalBloc = GlobalBloc();

要使用此BLoC,您只需導入該類并直接調(diào)用其方法,如下所示:

import 'global_bloc.dart';

class MyWidget extends StatelessWidget {
    @override
    Widget build(BuildContext context){
        globalBloc.push('building MyWidget');
        return Container();
    }
}

這是一個可以接受的解決方案,如果你需要有一個BLoC是唯一的,需要從應用程序內(nèi)的任意位置訪問。

  • 這是非常容易使用;
  • 它不依賴于任何BuildContext ;
  • 沒有必要通過任何BlocProvider去尋找 BLoC;
  • 為了釋放它的資源,只需確保將應用程序?qū)崿F(xiàn)為StatefulWidget,并在應用程序Widget 的重寫dispose()方法中調(diào)用globalBloc.dispose()

許多純粹主義者反對這種解決方案。我不知道為什么,但是...所以讓我們看看另一個......

2.1.2. 把它放在一切之上

在Flutter中,所有頁面的祖先本身必須是MaterialApp的父級。這是由于這樣的事實,一個頁面(或路由)被包裝在一個OverlayEntry,一個共同的孩子堆棧的所有頁面。

換句話說,每個頁面都有一個Buildcontext,它獨立于任何其他頁面。這就解釋了為什么在不使用任何技巧的情況下,2頁(路線)不可能有任何共同點。

因此,如果您需要在應用程序中的任何位置使用BLoC,則必須將其作為MaterialApp的父級,如下所示:

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.可用于子樹

大多數(shù)情況下,您可能需要在應用程序的某些特定部分使用BLoC。

作為一個例子,我們可以想到的討論主題,其中集團將用于

  • 與服務器交互以檢索,添加,更新帖子
  • 列出要在特定頁面中顯示的線程
  • ...

因此,如果您需要在應用程序中的任何位置使用BLoC,則必須將其作為MaterialApp的父級,如下所示:

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();
  }
}

這樣一來,所有widgets都可以通過對呼叫BlocProvider.of方法 訪問BLoC

附:
如上所示的解決方案并不是最佳解決方案,因為它將在每次重建時實例化BLoC。
后果:

  • 您將丟失任何現(xiàn)有的BLoC內(nèi)容
  • 它會耗費CPU時間,因為它需要在每次構(gòu)建時實例化它。

一個更好的辦法,在這種情況下,是使用StatefulWidget從它的持久受益國,具體如下:

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并直接重用現(xiàn)有實例。

2.3.僅適用于一個小部件

這涉及BLoC僅由一個 Widget使用的情況。

在這種情況下,可以在Widget中實例化BLoC。

3.事件狀態(tài)(允許根據(jù)事件響應狀態(tài)轉(zhuǎn)換)

有時,處理一系列可能是順序或并行,長或短,同步或異步以及可能導致各種結(jié)果的活動可能變得非常難以編程。您可能還需要更新顯示以及進度或根據(jù)狀態(tài)。

第一個用例旨在使這種情況更容易處理。

該解決方案基于以下原則:

  • 發(fā)出一個事件;
  • 此事件觸發(fā)一些導致一個或多個狀態(tài)的動作;
  • 這些狀態(tài)中的每一個都可以反過來發(fā)出其他事件或?qū)е铝硪粋€狀態(tài);
  • 然后,這些事件將根據(jù)活動狀態(tài)觸發(fā)其他操作;
  • 等等…

為了說明這個概念,我們來看兩個常見的例子:

應用初始化

  • 假設您需要運行一系列操作來初始化應用程序。操作可能與服務器的交互相關(guān)聯(lián)(例如,加載一些數(shù)據(jù))。
    在此初始化過程中,您可能需要顯示進度條和一系列圖像以使用戶等待。

認證

  • 在啟動時,應用程序可能需要用戶進行身份驗證或注冊。
    用戶通過身份驗證后,將重定向到應用程序的主頁面。然后,如果用戶注銷,則將其重定向到認證頁面。

為了能夠處理所有可能的情況,事件序列,但是如果我們認為可以在應用程序中的任何地方觸發(fā)事件,這可能變得非常難以管理。

這就是BlocEventState,兼有BlocEventStateBuilder,可以幫助很多...

3.1。BlocEventState

BlocEventState背后的想法是定義一個BLoC:

  • 接受事件作為輸入;
  • 當發(fā)出新事件時調(diào)用eventHandler;
  • eventHandler 負責根據(jù)事件采取適當?shù)男袆硬l(fā)出狀態(tài)作為回應。

下圖顯示了這個想法:


image.png

這是這類的源代碼。解釋如下:

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

  ///
  /// 要調(diào)用以發(fā)出事件
  ///
  Function(BlocEvent) get emitEvent => _eventController.sink.add;

  ///
  /// 當前/新狀態(tài)
  ///
  Stream<BlocState> get state => _stateController.stream;

  ///
  /// 事件的外部處理
  ///
  Stream<BlocState> eventHandler(BlocEvent event, BlocState currentState);

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

  //
  // 構(gòu)造函數(shù)
  //
  BlocEventStateBase({
    @required this.initialState,
  }){
    //
    // 對于每個接收到的事件,我們調(diào)用[eventHandler]并發(fā)出任何結(jié)果的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)來推送一個事件 ;
  • 一個流(狀態(tài))來監(jiān)聽發(fā)射狀態(tài)。

在初始化時(請參閱構(gòu)造函數(shù)):

一個初始化狀態(tài)需要設置;

  • 它創(chuàng)建了一個StreamSubscription聽傳入事件到
  • 將它們發(fā)送到eventHandler
  • 發(fā)出結(jié)果狀態(tài)。

3.2. 專門的BlocEventState

用于實現(xiàn)此類BlocEventState的模板在下面給出。之后,我們將實施真實的。

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() ......這將在幾分鐘內(nèi)出現(xiàn)。

此模板僅在初始化時提供initialState并覆蓋eventHandler。

這里有一些非常有趣的事情需要注意。我們使用異步生成器:async * 和yield語句。

使用async *修飾符標記函數(shù),將函數(shù)標識為異步生成器:

每次 yield 語句 被調(diào)用時,它增加了下面的表達式的結(jié)果 yield 輸出stream。

這是非常有用的,如果我們需要發(fā)出一個序列的States,從一系列的行動所造成(我們將在后面看到,在實踐中)

有關(guān)異步生成器的其他詳細信息,請單擊此鏈接。

3.3.BlocEvent和BlocState

正如您所注意到的,我們已經(jīng)定義了一個 BlocEvent 和 BlocState 抽象類。

這些類需要使用您要發(fā)出的特殊事件和狀態(tài)進行擴展。

3.4. BlocEventStateBuilder小部件

模式最后一部分的是BlocEventStateBuilder小部件,它允許你在響應State(s),所發(fā)射的BlocEventState。

這是它的源代碼:

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);
      },
    );
  }
}

這個Widget只是一個專門的StreamBuilder,它會在每次發(fā)出新的BlocState時調(diào)用builder輸入?yún)?shù)。


好的?,F(xiàn)在我們已經(jīng)擁有了所有的部分,現(xiàn)在是時候展示我們可以用它們做些什么......

3.5.案例1:應用程序初始化

第一個示例說明了您需要應用程序在啟動時執(zhí)行某些任務的情況。

常見的用途是游戲最初顯示啟動畫面(動畫與否),同時從服務器獲取一些文件,檢查新的更新是否可用,嘗試連接到任何游戲中心 ......在顯示實際主屏幕之前。為了不給應用程序什么都不做的感覺,它可能會顯示一個進度條并定期顯示一些圖片,同時它會完成所有初始化過程。

我要向您展示的實現(xiàn)非常簡單。它只會在屏幕上顯示一些競爭百分比,但這可以很容易地擴展到您的需求。

3.5.1。ApplicationInitializationEvent

在這個例子中,我只考慮2個事件:

  • start:此事件將觸發(fā)初始化過程;
  • stop:該事件可用于強制初始化進程停止。

這是定義代碼實現(xiàn):

class ApplicationInitializationEvent extends BlocEvent {
  
  final ApplicationInitializationEventType type;

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

enum ApplicationInitializationEventType {
  start,
  stop,
}

3.5.2. ApplicationInitializationState

該類將提供與初始化過程相關(guān)的信息。

對于這個例子,我會考慮:

  • 2標識:
    isInitialized指示初始化是否完成
    isInitializing以了解我們是否處于初始化過程的中間
  • 進度完成率

這是它的源代碼:

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. ApplicationInitializationBloc

該BLoC負責基于事件處理初始化過程。

這是代碼:

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 ”時,它從0開始計數(shù)到100(單位為10),并且對于每個值(0,10,20,......),它發(fā)出(通過yield)一個告訴的新狀態(tài)初始化正在運行(isInitializing = true)及其進度值。
  • 當收到事件"ApplicationInitializationEventType.stop"時,它認為初始化已完成。
  • 正如你所看到的,我在計數(shù)器循環(huán)中放了一些延遲。這將向您展示如何使用任何Future(例如,您需要聯(lián)系服務器的情況)
3.5.4. 將它們?nèi)堪b在一起

現(xiàn)在,剩下的部分是顯示顯示計數(shù)器的偽Splash屏幕 ......

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中初始化它;
  • 我們直接發(fā)出ApplicationInitializationEventType.start事件來觸發(fā)eventHandler
  • 每次發(fā)出ApplicationInitializationState時,我們都會更新文本
  • 初始化完成后,我們將用戶重定向到主頁。

特技

由于我們無法直接重定向到主頁,在構(gòu)建器內(nèi)部,我們使用WidgetsBinding.instance.addPostFrameCallback()方法請求Flutter 在渲染完成后立即執(zhí)行方法

3.6. 案例2:應用程序身份驗證和注銷

對于此示例,我將考慮以下用例:

  • 在啟動時,如果用戶未經(jīng)過身份驗證,則會自動顯示“ 身份驗證/注冊”頁面;
  • 在用戶認證期間,顯示CircularProgressIndicator ;
  • 經(jīng)過身份驗證后,用戶將被重定向到主頁 ;
  • 在應用程序的任何地方,用戶都可以注銷;
  • 當用戶注銷時,用戶將自動重定向到“ 身份驗證”頁面。

當然,很有可能以編程方式處理所有這些,但將所有這些委托給BLoC要容易得多。

下圖解釋了我要解釋的解決方案:

image

名為“ DecisionPage ” 的中間頁面將負責將用戶自動重定向到“ 身份驗證”頁面或主頁,具體取決于用戶身份驗證的狀態(tài)。當然,此DecisionPage從不顯示,也不應被視為頁面。

3.6.1. AuthenticationEvent

在這個例子中,我只考慮2個事件:

  • login:當用戶正確驗證時發(fā)出此事件;
  • logout:用戶注銷時發(fā)出的事件。

代碼如下:

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
該類將提供與身份驗證過程相關(guān)的信息。

對于這個例子,我會考慮:

  • 3點:
    isAuthenticated指示身份驗證是否完整
    isAuthenticating以了解我們是否處于身份驗證過程的中間
    hasFailed表示身份驗證失敗
  • 經(jīng)過身份驗證的用戶名

這是它的源代碼:

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.AuthenticationBloc

此BLoC負責根據(jù)事件處理身份驗證過程。

這是代碼:

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

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

    if (event is AuthenticationEventLogin) {
      //通知我們正在進行身份驗證
      yield AuthenticationState.authenticating();

      //模擬對身份驗證服務器的調(diào)用
      await Future.delayed(const Duration(seconds: 2));

      //告知我們是否已成功通過身份驗證
      if (event.name == "failure"){
        yield AuthenticationState.failure();
      } else {
        yield AuthenticationState.authenticated(event.name);
      }
    }

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

一些解釋:

  • 當收到事件“ AuthenticationEventLogin ”時,它會(通過yield)發(fā)出一個新狀態(tài),告知身份驗證正在運行(isAuthenticating = true)。
  • 然后它運行身份驗證,一旦完成,就會發(fā)出另一個狀態(tài),告知身份驗證已完成。
  • 當收到事件“ AuthenticationEventLogout ”時,它將發(fā)出一個新狀態(tài),告訴用戶不再進行身份驗證。
3.6.4. AuthenticationPage

正如您將要看到的那樣,為了便于解釋,此頁面非常基本且不會做太多。

這是代碼。解釋如下:

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行:它監(jiān)聽發(fā)出的AuthenticationState:
    如果身份驗證正在進行中,它會顯示一個CircularProgressIndicator,告訴用戶正在進行某些操作并阻止用戶訪問該頁面(第25-27行)
    如果驗證成功,我們不需要顯示任何內(nèi)容(第29-31行)。
    如果用戶未經(jīng)過身份驗證,則會顯示2個按鈕以模擬成功的身份驗證和失敗。
    當我們點擊其中一個按鈕時,我們發(fā)出一個AuthenticationEventLogin事件,以及一些參數(shù)(通常由認證過程使用)
    如果驗證失敗,我們會顯示錯誤消息(第60-64行)

提示

您可能已經(jīng)注意到,我將頁面包裝在WillPopScope中。
理由是我不希望用戶能夠使用Android'后退'按鈕,如此示例中所示,身份驗證是一個必須的步驟,它阻止用戶訪問任何其他部分,除非經(jīng)過正確的身份驗證。

3.6.5. DecisionPage

如前所述,我希望應用程序根據(jù)身份驗證狀態(tài)自動重定向到AuthenticationPage或HomePage。

以下是此DecisionPage的代碼,說明如下:

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());
          }
        }//此頁面不需要顯示任何內(nèi)容
        //總是在任何活動頁面后面提醒(因此“隱藏”)。
        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(= Route)的方式。要處理路由,我們使用導航器,它創(chuàng)建一個疊加層。
這個覆蓋是一個堆棧的OverlayEntry,他們每個人的包含頁面。
當我們通過Navigator.of(上下文)推送,彈出,替換頁面時,后者更新其重建的覆蓋(因此堆棧)。
當堆棧被重建,每個OverlayEntry(因此它的內(nèi)容)也被重建。
因此,當我們通過Navigator.of(上下文)進行操作時,所有剩余的頁面都會重建!

那么,為什么我將它實現(xiàn)為StatefulWidget?

為了能夠響應AuthenticationState的任何更改,此“ 頁面 ”需要在應用程序的整個生命周期中保持存在。

這意味著,根據(jù)上面的提醒,每次Navigator.of(上下文)完成操作時,都會重建此頁面。

因此,它的BlocEventStateBuilder也將重建,調(diào)用自己的構(gòu)建器方法。

因為此構(gòu)建器負責將用戶重定向到與AuthenticationState對應的頁面,所以如果我們每次重建頁面時重定向用戶,它將繼續(xù)重定向,因為不斷重建。

為了防止這種情況發(fā)生,我們只需要記住我們采取行動的最后一個AuthenticationState,并且只在收到另一個AuthenticationState時采取另一個動作。

這是如何運作的?

如上所述,每次發(fā)出AuthenticationState時,BlocEventStateBuilder都會調(diào)用其構(gòu)建器。

基于狀態(tài)標志(isAuthenticated),我們知道我們需要向哪個頁面重定向用戶。

特技

由于我們無法直接從構(gòu)建器重定向到另一個頁面,因此我們使用WidgetsBinding.instance.addPostFrameCallback()方法在呈現(xiàn)完成后請求Flutter執(zhí)行方法

此外,由于我們需要在重定向用戶之前刪除任何現(xiàn)有頁面,除了需要保留在所有情況下的此DecisionPage 之外,我們使用Navigator.of(context).pushAndRemoveUntil(...)來實現(xiàn)此目的。

3.6.6、登出
要讓用戶注銷,您現(xiàn)在可以創(chuàng)建一個“ LogOutButton ”并將其放在應用程序的任何位置。

  • 此按鈕只需要發(fā)出AuthenticationEventLogout()事件,這將導致以下自動操作鏈:
    1.它將由AuthenticationBloc處理
    2.反過來會發(fā)出一個AuthentiationState(isAuthenticated = false)
    3.這將由DecisionPage通過BlocEventStateBuilder處理
    4.這會將用戶重定向到AuthenticationPage
3.6.7. AuthenticationBloc

由于AuthenticationBloc需要提供給該應用程序的任何頁面,我們也將注入它作為MaterialApp父母,如下所示

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.表格驗證(允許根據(jù)條目和驗證控制表單的行為)

BLoC的另一個有趣用途是當您需要驗證表單時:

  • 根據(jù)某些業(yè)務規(guī)則驗證與TextField相關(guān)的條目;
  • 根據(jù)規(guī)則顯示驗證錯誤消息;
  • 根據(jù)業(yè)務規(guī)則自動化窗口小部件的可訪問性。

我現(xiàn)在要做的一個例子是RegistrationForm,它由3個TextFields(電子郵件,密碼,確認密碼)和1個RaisedButton組成,以啟動注冊過程。

我想要實現(xiàn)的業(yè)務規(guī)則是:

  • 該電子郵件必須是一個有效的電子郵件地址。如果不是,則需要顯示消息。
  • 該密碼必須是有效的(必須包含至少8個字符,具有1個大寫,小寫1,圖1和1個特殊字符)。如果無效,則需要顯示消息。
  • 在重新輸入密碼需要滿足相同的驗證規(guī)則和相同的密碼。如果不相同,則需要顯示消息。
  • 在登記時,按鈕可能只能激活所有的規(guī)則都是有效的。

4.1.RegistrationFormBloc

該BLoC負責處理驗證業(yè)務規(guī)則,如前所述。

源碼如下:

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();
  }
}

讓我詳細解釋一下......

  • 我們首先初始化3個BehaviorSubject來處理表單的每個TextField的Streams。
  • 我們公開了3個Function(String),它將用于接受來自TextFields的輸入。
  • 我們公開了3個Stream <String>,TextField將使用它來顯示由它們各自的驗證產(chǎn)生的潛在錯誤消息
  • 我們公開了1個Stream <bool>,它將被RaisedButton使用,以根據(jù)整個驗證結(jié)果啟用/禁用它。

好的,現(xiàn)在是時候深入了解更多細節(jié)......

您可能已經(jīng)注意到,此類的簽名有點特殊。我們來回顧一下吧。

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

with 關(guān)鍵字意味著這個類是使用混入(MIXINS)(在另一個類中重用一些類代碼的一種方法),為了能夠使用with關(guān)鍵字,該類需要擴展Object類。這些mixin包含分別驗證電子郵件和密碼的代碼。

有關(guān)詳細信息,混入我建議你閱讀從這篇大文章 Romain Rastel

4.1.1. Validator Mixins

我只會解釋EmailValidator,因為PasswordValidator非常相似。

First, the code:

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 函數(shù)(“ validateEmail ”),它是一個StreamTransformer。

提醒
StreamTransformer被調(diào)用如下:stream.transform(StreamTransformer)。
StreamTransformer通過transform方法從Stream引用它的輸入。然后它處理此輸入,并將轉(zhuǎn)換后的輸入重新注入初始Stream。

4.1.2. 為什么使用stream.transform()?

如前所述,如果驗證成功,StreamTransformer會將輸入重新注入Stream。為什么有用?

以下是與Observable.combineLatest3()相關(guān)的解釋...此方法在它引用的所有Streams之前不會發(fā)出任何值,至少發(fā)出一個值。

讓我們看看下面的圖片來說明我們想要實現(xiàn)的目標。

image.png

如果用戶輸入電子郵件并且后者經(jīng)過驗證,它將由電子郵件流發(fā)出,該電子郵件流將是Observable.combineLatest3()的一個輸入;
如果電子郵件地址無效,錯誤將被添加到流(和沒有價值會流出流);
這同樣適用于密碼和重新輸入密碼 ;
當所有這三個驗證都成功時(意味著所有這三個流都會發(fā)出一個值),Observable.combineLatest3()將依次發(fā)出一個真正的感謝“ (e,p,c)=> true ”(見第35行)。

4.1.3. 驗證2個密碼

我在互聯(lián)網(wǎng)上看到了很多與這種比較有關(guān)的問題。存在幾種解決方案,讓我解釋其中的兩種。

4.1.3.1.基本解決方案 - 沒有錯誤消息

第一個解決方案可能是以下一個:

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

這個解決方案只需驗證兩個密碼,如果它們匹配,就會發(fā)出一個值(= true)。

我們很快就會看到,Register按鈕的可訪問性將取決于registerValid流。

如果兩個密碼不匹配,則該流不會發(fā)出任何值,并且“ 注冊”按鈕保持不活動狀態(tài),但用戶不會收到任何錯誤消息以幫助他理解原因。

4.1.3.2。帶錯誤消息的解決方案

另一種解決方案包括擴展confirmPassword流的處理,如下所示:

Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
    .doOnData((String c){
      //如果接受密碼(在驗證規(guī)則后)
      //我們需要確保密碼和重新輸入的密碼匹配
      if (0 != _passwordController.value.compareTo(c)){
         //如果它們不匹配,請?zhí)砑渝e誤
        _passwordConfirmController.addError("No Match");
      }
    });

一旦驗證了重新輸入密碼,它就會被Stream發(fā)出,并且使用doOnData,我們可以直接獲取此發(fā)出的值并將其與密碼流的值進行比較。如果兩者不匹配,我們現(xiàn)在可以發(fā)送錯誤消息。

4.2. The RegistrationForm

現(xiàn)在讓我們先解釋一下RegistrationForm:

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>中,以便能夠響應驗證過程的任何結(jié)果(請參閱errorText:snapshot.error)
  • 每次對TextField的內(nèi)容進行修改時,我們都會通過onChanged發(fā)送輸入到BLoC進行驗證:_registrationFormBloc.onEmailChanged(電子郵件輸入的情況)
  • 對于RegisterButton,后者也包含在StreamBuilder <bool>中。
  • 如果_registrationFormBloc.registerValid發(fā)出一個值,onPressed方法將執(zhí)行某些操作
  • 如果未發(fā)出任何值,則onPressed方法將被指定為null,這將取消激活該按鈕。

而已!表單中沒有任何業(yè)務規(guī)則,這意味著可以更改規(guī)則而無需對表單進行任何修改,這非常好!

5.Part Of(允許Widget根據(jù)其在列表中的存在來調(diào)整其行為)

有時,Widget知道它是否是驅(qū)動其行為的集合的一部分是有趣的。

對于本文的最后一個用例,我將考慮以下場景:

應用程序處理項目;
用戶可以選擇放入購物籃的物品;
一件商品只能放入購物籃一次;
存放在購物籃中的物品可以從購物籃中取出;
一旦被移除,就可以將其取回。

對于此示例,每個項目將顯示一個按鈕,該按鈕將取決于購物籃中物品的存在。如果不是購物籃的一部分,該按鈕將允許用戶將其添加到購物籃中。如果是購物籃的一部分,該按鈕將允許用戶將其從籃子中取出。

為了更好地說明“ 部分 ”模式,我將考慮以下架構(gòu):

一個購物頁面將顯示所有可能的項目清單;
購物頁面中的每個商品都會顯示一個按鈕,用于將商品添加到購物籃或?qū)⑵湟瞥?,具體取決于其在購物籃中的位置;
如果一個項目在購物頁被添加到籃,它的按鈕將自動更新,以允許用戶從所述籃(反之亦然)將其刪除,而不必重新生成購物頁
另一頁,購物籃,將列出籃子里的所有物品;
可以從此頁面中刪除購物籃中的任何商品。

邊注
Part Of這個名字是我給的個人名字。這不是官方名稱。

正如您現(xiàn)在可以想象的那樣,我們需要考慮一個專門用于處理所有可能項目列表的BLoC,以及購物籃的一部分。

這個BLoC可能如下所示:

class ShoppingBloc implements BlocBase {
  // 所有商品的清單,購物籃的一部分
  Set<ShoppingItem> _shoppingBasket = Set<ShoppingItem>();

  // 流到所有可能項目的列表
  BehaviorSubject<List<ShoppingItem>> _itemsController = BehaviorSubject<List<ShoppingItem>>();
  Stream<List<ShoppingItem>> get items => _itemsController;

  // Stream以列出購物籃中的項目部分
  BehaviorSubject<List<ShoppingItem>> _shoppingBasketController = BehaviorSubject<List<ShoppingItem>>(seedValue: <ShoppingItem>[]);
  Stream<List<ShoppingItem>> get shoppingBasket => _shoppingBasketController;

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

  //構(gòu)造函數(shù)
  ShoppingBloc() {
    _loadShoppingItems();
  }

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

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

  void _postActionOnBasket(){
    // 使用新內(nèi)容提供購物籃流
    _shoppingBasketController.sink.add(_shoppingBasket.toList());
    
    // 任何其他處理,如
    // 計算籃子的總價
    // 項目數(shù)量,籃子的一部分......
  }

  //
  //生成一系列購物項目
  //通常這應該來自對服務器的調(diào)用
  //但是對于這個樣本,我們只是模擬
  //
  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 的內(nèi)容,以便通知所有正在監(jiān)聽此Stream更改的Widgets并能夠刷新/重建。

5.2. ShoppingPage

此頁面非常簡單,只顯示所有項目。

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顯示按鈕,:
    顯示出現(xiàn)在購物籃中的商品數(shù)量
    單擊時將用戶重定向到ShoppingBasket頁面
  • 項目列表使用GridView構(gòu)建,包含在StreamBuilder <List <ShoppingItem >>中
  • 每個項目對應一個ShoppingItemWidget

5.3.ShoppingBasketPage

此頁面與ShoppingPage非常相似,只是StreamBuilder現(xiàn)在正在偵聽由ShoppingBloc公開的_shoppingBasket流的變體。

5.4. ShoppingItemWidget和ShoppingItemBloc

Part Of 模式依賴于這兩個元素的組合

  • 該ShoppingItemWidget負責:
    顯示項目和
    用于在購物籃中添加項目或從中取出項目的按鈕
  • 該ShoppingItemBloc負責告訴ShoppingItemWidget后者是否是購物籃的一部分,或者不是。
    讓我們看看他們?nèi)绾我黄鸸ぷ?.....
5.4.1. ShoppingItemBloc

ShoppingItemBloc由每個ShoppingItemWidget實例化,賦予它“身份”

此BLoC偵聽ShoppingBasket流的所有變體,并檢查特定項目標識是否是籃子的一部分。

如果是,它會發(fā)出一個布爾值(= true),它將被ShoppingItemWidget捕獲,以確定它是否是籃子的一部分。

這是BLoC的代碼:

class ShoppingItemBloc implements BlocBase {
   // Stream,如果ShoppingItemWidget是購物籃的一部分,則通知
  BehaviorSubject<bool> _isInShoppingBasketController = BehaviorSubject<bool>();
  Stream<bool> get isInShoppingBasket => _isInShoppingBasketController;

  //收到所有商品列表的流,購物籃的一部分
  PublishSubject<List<ShoppingItem>> _shoppingBasketController = PublishSubject<List<ShoppingItem>>();
  Function(List<ShoppingItem>) get shoppingBasket => _shoppingBasketController.sink.add;

   //具有shoppingItem的“標識”的構(gòu)造方法
  ShoppingItemBloc(ShoppingItem shoppingItem){
    //每次購物籃內(nèi)容的變化
    _shoppingBasketController.stream
                           //我們檢查這個shoppingItem是否是購物籃的一部分
                         .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

此Widget負責:

  • 創(chuàng)建ShoppingItemBloc的實例并將其自己的標識傳遞給BLoC
  • 監(jiān)聽ShoppingBasket內(nèi)容的任何變化并將其轉(zhuǎn)移到BLoC
  • 監(jiān)聽ShoppingItemBloc知道它是否是籃子的一部分
  • 顯示相應的按鈕(添加/刪除),具體取決于它在籃子中的存在
  • 響應按鈕的用戶操作
    當用戶點擊添加按鈕時,將自己添加到購物籃中
    當用戶點擊刪除按鈕時,將自己從籃子中移除。

讓我們看看它是如何工作的(解釋在代碼中給出)。

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

    //由于不應在“initState()”方法中使用上下文,
    //在需要時更喜歡使用“didChangeDependencies()”
    //在初始化時引用上下文
    _initBloc();
  }

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

    //因為Flutter可能決定重新組織Widgets樹
    //最好重新創(chuàng)建鏈接
    _disposeBloc();
    _initBloc();
  }

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

   //這個例程對于創(chuàng)建鏈接是可靠的
  void _initBloc() {
    //創(chuàng)建ShoppingItemBloc的實例
    _bloc = ShoppingItemBloc(widget.shoppingItem);

    //檢索處理購物籃內(nèi)容的BLoC 
    _shoppingBloc = BlocProvider.of<ShoppingBloc>(context);

     //傳輸購物內(nèi)容的簡單管道
    //購物籃子到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. 這一切如何運作?

下圖顯示了所有部分如何協(xié)同工作。

image.png

結(jié)論

另一篇長篇文章,我希望我能縮短一點,但我認為值得一些解釋。

正如我在介紹中所說,我個人在我的開發(fā)中經(jīng)常使用這些“ 模式 ”。這讓我節(jié)省了大量的時間和精力; 我的代碼更易讀,更容易調(diào)試。

此外,它有助于將業(yè)務與視圖分離。

大多數(shù)肯定有其他方法可以做到這一點,甚至更好的方式,但它只對我有用,這就是我想與你分享的一切。

請繼續(xù)關(guān)注新文章,同時祝您編程愉快。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375