1. 程式人生 > >[譯]Flutter 響應式程式設計:Steams 和 BLoC 實踐範例

[譯]Flutter 響應式程式設計:Steams 和 BLoC 實踐範例

原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier BoelensReactive Programming - Streams - BLoC 寫的後續

閱讀本文前建議先閱讀前篇,前篇中文翻譯有兩個版本:

  1. [譯]Flutter響應式程式設計:Streams和BLoC by JarvanMo

    較忠於原作的版本

  2. Flutter中如何利用StreamBuilder和BLoC來控制Widget狀態 by 吉原拉麵

    省略了一些初級概念,補充了一些個人解讀

前言

在瞭解 BLoC, Reactive ProgrammingStreams 概念後,我又花了些時間繼續研究,現在非常高興能夠與大家分享一些我經常使用並且很有用的模式(至少我是這麼認為的)。這些模式為我節約了大量的開發時間,並且讓程式碼更加易讀和除錯。

在這篇文章中我要分享的有:

  1. BlocProvider 效能優化

    結合 StatefulWidget 和 InheritedWidget 兩者優勢構建 BlocProvider

  2. BLoC 的範圍和初始化

    根據 BLoC 的使用範圍初始化 BLoC

  3. 事件與狀態管理

    基於事件(Event) 的狀態 (State) 變更響應

  4. 表單驗證

    根據表單項驗證來控制表單行為 (範例中包含了表單中常用的密碼和重複密碼比對)

  5. Part Of 模式

    允許元件根據所處環境(是否在某個列表/集合/元件中)調整自身的行為

文中涉及的完整程式碼可在 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;
  }
}

複製程式碼

這種方案的優點是:StatefulWidgetdispose() 方法可以確保在 BLoC 初始化時分配的記憶體資源在不需要時可以釋放掉。

譯者注

這個優點是單獨基於 InheritedWidget 很難實現的,因為 InheritedWidget 沒有提供 dispose 方法,而 Dart 語言又沒有自帶的解構函式

雖然這種方案執行起來沒啥問題,但從效能角度卻不是最優解。

這是因為 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

譯者注

即在原來 StatefulWidgetchild 外面再包了一個 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;
}
複製程式碼

當然這也是源於 Fluter 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. 應用中任何地方可用

在實際應用中,常常需要處理如使用者鑑權、使用者檔案、使用者設定項、購物籃等等需要在 App 中任何元件都可訪問的資料或狀態,這裡總結了適用這種情況的兩種 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 實現成全域性可用,只需在涉及論壇的檢視樹中可用就行了。

那麼可採用通過 BlocProviderBLoC 作為模組子樹的根(父級)注入的方式,如下所示:

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 重構(rebuild)時都會重新初始化 BLoC ,帶來的結果是:

  • 丟失 BLoC 中已經存在的資料內容
  • 重新初始化BLoC 要佔用 CPU 時間

在這個例子中更好的方式是使用 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)

有時侯需要我們編碼實現一些棘手的業務流程,這些流程可能會由序列或並行、耗時長短不一、同步或非同步的子流程構成的,很可能每個子流程的處理結果也是千變萬化的,而且還可能需要根據其處理進度或狀態進行檢視更新。

而本文中「事件與狀態管理」解決方案的目的就是讓處理這種複雜的業務流程變得更簡單。

方案是基於以下流程和規則的:

  • 發出某個事件
  • 該事件觸發一些動作 (action) ,這些動作會導致一個或多個狀態產生/變更
  • 這些狀態又觸發其它事件,或者產生/變更為其它狀態
  • 然後這些事件又根據狀態的變更情況,觸發其它動作
  • 等等…

為了更好的展示這些概念,我還舉了兩個具體的例子:

  • 應用初始化 (Application initialization)

    很多時候我們都需要執行一系列動作來初始化 App, 這些動作可能是與伺服器的互動相關聯的 (例如:獲取並載入一些資料)。而且在初始化過程中,可能還需要顯示進度條及載入動畫讓使用者能耐心等待。

  • 使用者身份驗證 (Authentication)

    在 App 啟動後需要使用者登入或註冊,使用者成功登入後,將跳轉(重定向)到 App 的主頁面; 而使用者登出則將跳轉(重定向)到驗證頁面。

為了應對所有的可能,我們將管理一系列的事件,而這些事件可能是在 App 中任何地方觸發的,這使得事件和狀態的管理異常複雜,所幸我們可以藉助結合了 BlocEventStateBuiderBlocEventState 類大大降低事件和狀態管理的難度。

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) 監聽已發出的狀態 State(s) 作為狀態的輸出出口

在這個類初始化時 (參考程式碼中 Constructor 部分)

  • 需要提供初始狀態 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 方法。

還需要注意的是,我們使用了 非同步生成器 (asynchronous generator) 語法:async*yield

使用 async* 修飾符可將某個方法標記為一個 非同步生成器(asynchronous generator) 方法,比如上面的程式碼中每次呼叫 eventHandler 方法內 yield 語句時,它都會把 yield 後面的表示式結果新增到輸出 Stream 中。

如果我們需要通過一系列動作觸發一系列 States (後面會在範例中看到),這一點特別有用。

有關 非同步生成器 的其他詳細資訊,可參考 這篇文章

3.3. BlocEvent 和 BlocState

你可能注意到了,我們還定義了 BlocEventBlocState 兩個抽象類,這兩個抽象類都是要根據實際情況,也就是在實際業務場景中根據你想要觸發的事件和丟擲的狀態來具體 擴充套件實現 的。

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)

第一個例子演示了 App 在啟動時執行某些任務的情況。

一個常見的場景就是遊戲的啟動畫面,也稱 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 個 flag:
    • 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 將從 0100 開始計數(每次步進 10),而且未到 100 時每次都將通過 yield 丟擲一個新狀態 (state) 告知初始化正在進行 (isInitializing = true) 及完成進度 prograss 具體的值
  • 當接收到 ApplicationInitializationEventType.stop 事件時,會認為初始化已經完成。
  • 如你所見,我在迴圈過程中加了些延遲 (delay) ,目的是演示 Future的適用場景(如從伺服器獲取資料)

3.5.4. 組合使用

現在,剩下的事情就是把代表進度完成率的計數器顯示到假的 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}%');
              },
            ),
          ),
        ),
      ),
    );
  }
}
複製程式碼

說明:

  • 在 App 中,ApplicationInitializationBloc 並不是任何元件都需要用到,所以只在一個 StatefulWidget 中初始化(例項化)了該 BLoC
  • 直接發出 ApplicationInitializationEventType.start 事件來觸發 eventHandler
  • 每次 ApplicationInitializationState 被丟擲,都會更新文字內容
  • 初始化過程完成後,跳轉(重定向)到了 Home 介面

小技巧

由於無法直接跳轉到 Home 介面,在 builder 方法中,使用了WidgetsBinding.instance.addPostFrameCallback() 方法來請求 Flutter 在完成渲染後執行跳轉。參考 addPostFrameCallback()


3.6. 事件與狀態管理例2: 使用者身份驗證(登入與登出)

在這個例子中,我考慮瞭如下場景:

  • 如果使用者沒有登入,則自動顯示 登入/註冊(Authentication/Registration) 介面
  • 使用者提交登入資訊後,顯示一個代表正在處理的迴圈進度指示器(轉圈圈)
  • 一旦使用者登入成功,將跳轉到 Home 介面
  • 在 App 任何地方,使用者都可能登出
  • 如果使用者登出,將自動跳轉到 登入(Authentication) 介面

當然以其它程式設計方式也可以實現這些功能,但以 BLoC 的方式來實現可能更簡單。

下圖解釋了將要實現的方案流程:

BlocAuthentication

中間跳轉頁面 DecisionPage 將負責 自動 將使用者重定向到 Authentication 介面或 Home 介面,具體到哪個介面取決於使用者的登入狀態。當然 DecisionPage 不會顯示給使用者,也不應該將其視為一個真正的頁面。

同樣首先要做的是定義一些事件和狀態…

3.6.1. 定義事件: AuthenticationEvent

作為例子,我只考慮了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 個 flag:
    • 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 裝置提供的 Back 鍵來跳過驗證訪問到其它頁面。

3.6.5. 中間跳轉頁面: DecisionPage

如前所述,我希望 App 根據使用者登入狀態自動跳轉到 AuthenticationPageHomePage

程式碼及說明如下:

bloc_decision_page.dart

class DecisionPage 
            
           

相關推薦

[]Flutter 響應程式設計Steams BLoC 實踐範例

原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier Boelens 為 Reactive Programming - Streams - BLoC 寫的後續 閱讀本文前建議先閱讀前篇,前篇中文翻譯有兩個版

[]Flutter響應程式設計StreamsBLoC

想看原文請出門右轉原文傳送門 版本所有,轉載請註明出處。本文主要介紹Streams,Bloc和Reactive Programming(響應式程式設計)的概念。 理論和實踐範例。 難度:中級 介紹 我花了很長時間才找到介紹Reactive Programming,B

iOS響應程式設計ReactiveCocoa vs RxSwift 選誰好

要直接比較這兩個有點難。Rx 是 Reactive Extensions 的一部分,其他語言像C#, Java 和 JS 也有。Reactive Cocoa 受 Functional Reactive Programming(FRP) 啟發,但是在最近一段時間裡,他們提到也受

響應程式設計理解響應程式設計

引言 響應式程式設計並不是一個新概念。早在90年代末,微軟的一名電腦科學家就提出了響應式程式設計。用來設計和開發微軟的某些庫。 定義 響應式程式設計(Reactive Programming,RP)的定義有很多個版本,如wiki、stackover

響應設計理解設備像素,CSS像素屏幕分辨率

rtk mos ava hdr nsq gms sco dpt nsca 概述屏幕分辨率、設備像素(device-width)和CSS像素(width)這些術語,在非常多語境下。是可互換的,但也因此easy在有差異的地方引起混淆,實際上它們是不同的概念。屏幕分辨率和設備像

Rxjava2入門函式響應程式設計及概述

Rxjava2入門教程一:https://www.jianshu.com/p/15b2f3d7141a Rxjava2入門教程二:https://www.jianshu.com/p/c8150187714c Rxjava2入門教程三:https://www.jianshu.com/p/6e7

響應程式設計系列(一)什麼是響應程式設計?reactor入門

響應式程式設計 系列文章目錄 (一)什麼是響應式程式設計?reactor入門 (二)Flux入門學習:流的概念,特性和基本操作 (三)Flux深入學習:流的高階特性和進階用法 (四)reactor-core響應式api如何測試和除錯? (五)Spring reactive: Spring WebFl

Reactive Stack系列(一)響應程式設計從入門到放棄

為了詳細介紹下基於Spring Framework 5 & Spring Boot 2 的WebFlux的響應式程式設計,先畫下如下邏輯圖,後文將以邏輯圖箭頭方向逐一解釋關於響應式程式設計的點點滴滴。 1. Spring Framework5 自 2013 年12月Spring Fra

Angular 資料繫結、響應程式設計管道

一.資料繫結 1.資料繫結基本內容 <h1>{{productTitle}}!</h1>使用插值表示式將一個表示式的值顯示在模板上 <img [src]="imgUrl"

Rxjava2入門教程二Observable與Observer響應程式設計在Rxjava2中的典型實現

在RxJava中,函式響應式程式設計具體表現為一個觀察者(Observer)訂閱一個可觀察物件(Observable),通過建立可觀察物件發射資料流,經過一系列操作符(Operators)加工處理和執行緒排程器(Scheduler)在不同執行緒間的轉發,最後由觀察者接受並做出響應的一個過程 Observ

[demo]自定義響應網頁利用css3媒體查詢window.matchMedia實現

需求: 自定義響應式網頁,需要針對不同的螢幕尺寸做不同的處理。 分析: 1,樣式處理,必須是css3媒體查詢,簡單有效; 2,行為和結構的處理,我們選用window.matchMedia函式處理

【Java開發者專場】阿里專家杜萬Java響應程式設計,一文全面解讀

本篇文章來自於2018年12月22日舉辦的《阿里雲棲開發者沙龍—Java技術專場》,杜萬專家是該專場第四位演講的嘉賓,本篇文章是根據杜萬專家在《阿里雲棲開發者沙龍—Java技術專場》的演講視訊以及PPT整理而成。 摘要:響應式宣言如何解讀,Java中如何進行響應式程式設計,Reactor Stream

阿里專家杜萬Java響應程式設計,一文全面解讀

本篇文章來自於2018年12月22日舉辦的《阿里雲棲開發者沙龍—Java技術專場》,杜萬專家是該專場第四位演講的嘉賓,本篇文章是根據杜萬專家在《阿里雲棲開發者沙龍—Java技術專場》的演講視訊以及PPT整理而成。 摘要:響應式宣言如何解讀,Java中如何進行響應式程式設計,Reacto

函數語言程式設計響應程式設計

在程式開發中,a=b+c;賦值之後,b或者c的值變化後,a的值不會跟著變化。響應式程式設計目標就是,如果b或者c的數值發生變化,a的數值會同時發生變化。 函數語言程式設計 函數語言程式設計是一系列被不公平對待的程式設計思想的保護傘,它的核心思想是,它是一

Android函式響應程式設計——RxJava的4大subject執行緒控制

Subject Subject既可以是一個Observer也可以是一個Observable,它用來連線兩者。所以Subject被認為是Subject=Observable+Observer 1.PublishSubject PublishSubject在被建立完成之後立刻開

一、Rxjava從頭學響應程式設計

響應式程式設計,現在被經常提起,同時越來越多的出現在我們的程式碼構建中。同時現在有很多主流的響應式框架,如RX等,如果不能夠理解響應式程式設計的話,對此類框架的使用總是有一些迷惑。那麼,到底什麼是響應式程式設計? 搜尋網路會有一大片的響應式程式設計的解

講課Webflux響應程式設計(SpringBoot 2.0新特性)

學習webflux前需要學習三個基礎: 函數語言程式設計和lambda表示式 Stream流程式設計 Reactive stream 響應式流 接下來進入學習 一、函數語言程式設計和lambda表示式 1. 什麼是函數語言程式設計 函數語言程式設計是

Spring Boot (十四) 響應程式設計以及 Spring Boot Webflux 快速入門

1. 什麼是響應式程式設計 在計算機中,響應式程式設計或反應式程式設計(英語:Reactive programming)是一種面向資料流和變化傳播的程式設計正規化。這意味著可以在程式語言中很方便地表達靜態或動態的資料流,而相關的計算模型會自動將變化的值通過資料流進行傳播。 例如,在指令式程式設計環境中,a

全功能響應模板黑暗元素

bsp 圖片 sass resp temp net pad thumb 需求 預覽: 部分頁面展示: 演示及下載: 演示地址 免費下載 更多模板請立刻訪問 模板集市 介紹: 全功能響應式模板,支持ip

響應Web設計:HTML5CSS3實戰 第2版 (本·弗萊恩) 中文pdf完整版

教程 理解 第2章 web設計 掌握 不可 css3過渡 div and 本書將當前Web 設計中熱門的響應式設計技術與HTML5 和CSS3 結合起來,為讀者全面深入地講解了針對各種屏幕大小設計和開發現代網站的各種技術。書中不僅討論了媒體查詢、彈性布局、響應式圖片,更