[譯]Flutter 響應(yīng)式編程:Steams 和 BLoC 實(shí)踐范例(3) - 事件與狀態(tài)管理

原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier BoelensReactive Programming - Streams - BLoC 寫的后續(xù)

閱讀本文前建議先閱讀前篇狼忱,前篇中文翻譯有兩個(gè)版本:

  1. [譯]Flutter響應(yīng)式編程:Streams和BLoC by JarvanMo
    忠于原作的版本

  2. Flutter中如何利用StreamBuilder和BLoC來控制Widget狀態(tài) by 吉原拉面
    省略了一些初級概念衔肢,補(bǔ)充了一些個(gè)人解讀

前言

在了解 BLoC, Reactive ProgrammingStreams 概念后度陆,我又花了些時(shí)間繼續(xù)研究,現(xiàn)在非常高興能夠與你們分享一些我經(jīng)常使用并且個(gè)人覺得很有用的模式(至少我是這么認(rèn)為的)。這些模式為我節(jié)約了大量的開發(fā)時(shí)間曲管,并且讓代碼更加易讀和調(diào)試判沟。

目錄

(由于原文較長唾糯,翻譯發(fā)布時(shí)進(jìn)行了分割)

  1. BlocProvider 性能優(yōu)化
    結(jié)合 StatefulWidgetInheritedWidget 兩者優(yōu)勢構(gòu)建 BlocProvider

  2. BLoC 的范圍和初始化
    根據(jù) BLoC 的使用范圍初始化 BLoC

  3. 事件與狀態(tài)管理
    基于事件(Event) 的狀態(tài) (State) 變更響應(yīng)

  4. 表單驗(yàn)證
    根據(jù)表單項(xiàng)驗(yàn)證來控制表單行為 (范例中包含了表單中常用的密碼和重復(fù)密碼比對)

  5. Part Of 模式
    允許組件根據(jù)所處環(huán)境(是否在某個(gè)列表/集合/組件中)調(diào)整自身的行為

文中涉及的完整代碼可在 GitHub 查看。

3. 事件與狀態(tài)管理(Event - State)

有時(shí)侯需要我們編碼實(shí)現(xiàn)一些棘手的業(yè)務(wù)流程淮摔,這些流程可能會(huì)由串行或并行私沮、耗時(shí)長短不一、同步或異步的子流程構(gòu)成的噩咪,很可能每個(gè)子流程的處理結(jié)果也是千變?nèi)f化的顾彰,而且還可能需要根據(jù)其處理進(jìn)度或狀態(tài)進(jìn)行視圖更新。

而本文中「事件與狀態(tài)管理」解決方案的目的就是讓處理這種復(fù)雜的業(yè)務(wù)流程變得更簡單胃碾。

方案是基于以下流程和規(guī)則的:

  • 發(fā)出某個(gè)事件
  • 該事件觸發(fā)一些動(dòng)作(action)涨享,這些動(dòng)作會(huì)導(dǎo)致一個(gè)或多個(gè)狀態(tài)產(chǎn)生/變更
  • 這些狀態(tài)又觸發(fā)其它事件,或者產(chǎn)生/變更為其它狀態(tài)
  • 然后這些事件又根據(jù)狀態(tài)的變更情況仆百,觸發(fā)其它動(dòng)作
  • 等等…

為了更好的展示這些概念厕隧,我還舉了兩個(gè)具體的例子:

  • 應(yīng)用初始化 (Application initialization)

    很多時(shí)候我們都需要運(yùn)行一系列動(dòng)作來初始化 App, 這些動(dòng)作可能是與服務(wù)器的交互相關(guān)聯(lián)的 (例如:獲取并加載一些數(shù)據(jù))俄周。而且在初始化過程中吁讨,可能還需要顯示進(jìn)度條及載入動(dòng)畫讓用戶能耐心等待。

  • 用戶身份驗(yàn)證 (Authentication)

    在 App 啟動(dòng)后需要用戶登錄或注冊峦朗,用戶成功登錄后建丧,將跳轉(zhuǎn)(重定向)到 App 的主頁面; 而用戶注銷則將跳轉(zhuǎn)(重定向)到驗(yàn)證頁面波势。

為了應(yīng)對所有的可能翎朱,我們將管理一系列的事件,而這些事件可能是在 App 中任何地方觸發(fā)的尺铣,這使得事件和狀態(tài)的管理異常復(fù)雜拴曲,所幸我們可以借助結(jié)合了 BlocEventStateBuiderBlocEventState 類大大降低事件和狀態(tài)管理的難度。

3.1. BlocEventState 抽象類

BlocEventState 背后的邏輯是將 BLoC 定義成這樣一套機(jī)制:

  • 接收事件(event)作為輸入
  • 當(dāng)新的事件觸發(fā)(輸入)時(shí)凛忿,調(diào)用一個(gè)對應(yīng)的事件處理器 eventHandler
  • 事件處理器(eventHandler)負(fù)責(zé)根據(jù)事件(event)采用適當(dāng)?shù)奶幚?em>(actions)后澈灼,拋出一個(gè)或多個(gè)狀態(tài)(State)作為響應(yīng)

如下圖所示:

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

如代碼所示,我們定義的其實(shí)是一個(gè)抽象類店溢,是需要擴(kuò)展實(shí)現(xiàn)的叁熔,實(shí)現(xiàn)的重點(diǎn)就是定義 eventHandler 這個(gè)方法的具體行為。

當(dāng)然我們還可以看到:

  • Sink (代碼中的 emitEvent) 作為事件 Event 的輸入入口
  • Stream (代碼中的 state) 監(jiān)聽已發(fā)出的狀態(tài) State(s) 作為狀態(tài)的輸出出口

在這個(gè)類初始化時(shí)(參考代碼中 Constructor 部分)

  • 需要提供初始狀態(tài) initialState
  • 創(chuàng)建了一個(gè) StreamSubscription 用來監(jiān)聽輸入的事件 (Events) 并:
    • 將事件分配給事件處理器 eventHandler
    • 拋出結(jié)果 state(s)

3.2. BlocEventState 的擴(kuò)展實(shí)現(xiàn)

下方的模板代碼就是基于擴(kuò)展 BlocEventStateBase 抽象類實(shí)現(xiàn)了一個(gè)具體的 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();
  }
}

模板代碼會(huì)報(bào)錯(cuò)逞怨,請不要擔(dān)心者疤,這是正常的…因?yàn)槲覀冞€沒有定義 BlocState.notInitialized()…后面會(huì)給出的。

這個(gè)模板只是在初始化時(shí)簡單地給出了一個(gè)初始狀態(tài) initialState叠赦,并覆寫了 eventHandler 方法驹马。

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

使用 async* 修飾符可將某個(gè)方法標(biāo)記為一個(gè)異步生成器(asynchronous generator)方法,比如上面的代碼中每次調(diào)用 eventHandler 方法內(nèi) yield 語句時(shí)糯累,它都會(huì)把 yield 后面的表達(dá)式結(jié)果添加到輸出 Stream 中算利。

如果我們需要通過一系列動(dòng)作觸發(fā)一系列States(后面會(huì)在范例中看到),這一點(diǎn)特別有用泳姐。

有關(guān)異步生成器的其他詳細(xì)信息效拭,可參考 這篇文章

3.3. BlocEvent 和 BlocState

你可能注意到了胖秒,我們還定義了 BlocEventBlocState 兩個(gè)抽象類缎患,這兩個(gè)抽象類都是要根據(jù)實(shí)際情況,也就是在實(shí)際業(yè)務(wù)場景中根據(jù)你想要觸發(fā)的事件和拋出的狀態(tài)來具體擴(kuò)展實(shí)現(xiàn)的阎肝。

3.4. BlocEventStateBuilder 組件

這個(gè)模式的最后一部分就是 BlocEventStateBuilder 組件了挤渔,這個(gè)組件可以根據(jù) BlocEventState 拋出的 State(s)作出視圖層面的響應(yīng)。

代碼如下:

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

其實(shí)這個(gè)組件除了一個(gè) StreamBuilder 外沒啥特別的风题,這個(gè) StreamBuilder 的作用就是每當(dāng)有新的 BlocState 拋出后判导,將其作為新的參數(shù)值調(diào)用 builder 方法。


好了沛硅,這些就是這個(gè)模式的全部構(gòu)成眼刃,接下來我們看看可以用它們來做些啥…

3.5. 事件與狀態(tài)管理例1: 應(yīng)用初始化(Application Initialization)

第一個(gè)例子演示了 App 在啟動(dòng)時(shí)執(zhí)行某些任務(wù)的情況。

一個(gè)常見的場景就是游戲的啟動(dòng)畫面摇肌,也稱 Splash 界面(不管是不是動(dòng)畫的)擂红,在顯示真正的游戲主界面前,游戲應(yīng)用會(huì)從服務(wù)器獲取一些文件围小、檢查是否需要更新篮条、嘗試與系統(tǒng)的「游戲中心」通訊等等;而且在完成初始化前吩抓,為了不讓用戶覺得應(yīng)用啥都沒做,可能還會(huì)顯示進(jìn)度條赴恨、定時(shí)切換顯示一些圖片等疹娶。

我給出的實(shí)現(xiàn)是非常簡單的,只顯示了完成百分比的伦连,你可以根據(jù)自己的需要非常容易地進(jìn)行擴(kuò)展雨饺。

首先要做的就是定義事件和狀態(tài)…

3.5.1. 定義事件: ApplicationInitializationEvent

作為例子,這里我只考慮了 2 個(gè)事件:

  • start:觸發(fā)初始化處理過程
  • stop:用于強(qiáng)制停止初始化過程

它們的定義如下:

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. 定義狀態(tài): ApplicationInitializationState

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

同樣作為例子额港,這里我只考慮了:

  • 2 個(gè) flag:
    • isInitialized 用來標(biāo)識(shí)初始化是否完成
    • isInitializing用來知曉我們是否處于初始化過程中
  • 進(jìn)度完成率 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. 實(shí)現(xiàn) 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();
    }
  }
}

說明:

  • 當(dāng)接收到 ApplicationInitializationEventType.start 事件時(shí)歧焦,進(jìn)度完成率 prograss 將從 0100 開始計(jì)數(shù)(每次步進(jìn) 10)移斩,而且未到 100 時(shí)每次都將通過 yield 拋出一個(gè)新狀態(tài)(state)告知初始化正在進(jìn)行(isInitializing = true)及完成進(jìn)度 prograss 具體的值
  • 當(dāng)接收到 ApplicationInitializationEventType.stop 事件時(shí),會(huì)認(rèn)為初始化已經(jīng)完成。
  • 如你所見向瓷,我在循環(huán)過程中加了些延遲(delay)肠套,目的是演示 Future的適用場景(如從服務(wù)器獲取數(shù)據(jù))

3.5.4. 組合使用

現(xiàn)在,剩下的事情就是把代表進(jìn)度完成率的計(jì)數(shù)器顯示到假的 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 并不是任何組件都需要用到你稚,所以只在一個(gè) StatefulWidget 中初始化(實(shí)例化)了該 BLoC
  • 直接發(fā)出 ApplicationInitializationEventType.start 事件來觸發(fā) eventHandler
  • 每次 ApplicationInitializationState 被拋出,都會(huì)更新文字內(nèi)容
  • 初始化過程完成后朱躺,跳轉(zhuǎn)(重定向)到了 Home 界面

小技巧

由于無法直接跳轉(zhuǎn)到 Home 界面刁赖,在 builder 方法中,使用了WidgetsBinding.instance.addPostFrameCallback() 方法來請求 Flutter 在完成渲染后執(zhí)行跳轉(zhuǎn)长搀。參考 addPostFrameCallback()


3.6. 事件與狀態(tài)管理例2: 用戶身份驗(yàn)證(登錄與注銷)

在這個(gè)例子中宇弛,我考慮了如下場景:

  • 如果用戶沒有登錄,則自動(dòng)顯示登錄/注冊(Authentication/Registration)界面
  • 用戶提交登錄信息后盈滴,顯示一個(gè)代表正在處理的循環(huán)進(jìn)度指示器(轉(zhuǎn)圈圈)
  • 一旦用戶登錄成功涯肩,將跳轉(zhuǎn)到 Home 界面
  • 在 App 任何地方,用戶都可能注銷
  • 如果用戶注銷巢钓,將自動(dòng)跳轉(zhuǎn)到登錄(Authentication)界面

當(dāng)然以其它編程方式也可以實(shí)現(xiàn)這些功能病苗,但以 BLoC 的方式來實(shí)現(xiàn)可能更簡單。

下圖解釋了將要實(shí)現(xiàn)的方案流程:

BlocAuthentication

中間跳轉(zhuǎn)頁面DecisionPage將負(fù)責(zé)自動(dòng)將用戶重定向到 Authentication 界面或 Home 界面症汹,具體到哪個(gè)界面取決于用戶的登錄狀態(tài)硫朦。當(dāng)然 DecisionPage 不會(huì)顯示給用戶,也不應(yīng)該將其視為一個(gè)真正的頁面背镇。

同樣首先要做的是定義一些事件和狀態(tài)…

3.6.1. 定義事件: AuthenticationEvent

作為例子咬展,我只考慮了2個(gè)事件:

  • login:用戶成功登錄時(shí)會(huì)發(fā)出該事件
  • logout:用戶注銷時(shí)會(huì)發(fā)出該事件

它們的定義如下:

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. 定義狀態(tài): AuthenticationState

AuthenticationState 類將提供與驗(yàn)證過程相關(guān)的信息。

同樣作為例子瞒斩,我只考慮了:

  • 3 個(gè) flag:
    • isAuthenticated 用來標(biāo)識(shí)驗(yàn)證是否完成
    • isAuthenticating 用來知曉是否處于驗(yàn)證過程中
    • hasFailed 用來表示身份是否驗(yàn)證失敗
  • 經(jīng)過身份驗(yàn)證后的用戶名: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. 實(shí)現(xiàn) BLoC: AuthenticationBloc

BLoC 將基于事件類型來處理具體的身份驗(yàn)證過程破婆。

代碼如下:

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

說明:

  • 當(dāng)接收到 AuthenticationEventLogin事件時(shí),會(huì)通過 yield 拋出一個(gè)新狀態(tài)(state)告知身份驗(yàn)證正在進(jìn)行(isAuthenticating = true)
  • 當(dāng)身份驗(yàn)證一旦完成胸囱,會(huì)拋出另一個(gè)新的狀態(tài)(state)告知已經(jīng)完成了
  • 當(dāng)接收到AuthenticationEventLogout事件時(shí)祷舀,會(huì)拋出一個(gè)新狀態(tài)(state)告知用戶已經(jīng)不在是已驗(yàn)證狀態(tài)

3.6.4. 登錄頁面: AuthenticationPage

如你所見,為了便于說明烹笔,這個(gè)頁面并沒有做的很復(fù)雜裳扯。

代碼及說明如下:

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 行:監(jiān)聽被拋出的 AuthenticationState
    • 如果正在驗(yàn)證過程中,會(huì)顯示循環(huán)進(jìn)度指示器(轉(zhuǎn)圈圈)谤职,告知用戶正在處理中饰豺,并阻止用戶訪問到其它頁面(第25 ~ 27 行)
    • 如果驗(yàn)證成功,顯示一個(gè)空的 Container允蜈,即不顯示任何內(nèi)容 (第 29 ~ 31 行)
    • 如果用戶還沒有登錄冤吨,顯示2個(gè)按鈕蒿柳,可模擬登錄成功和失敗的情況
    • 當(dāng)點(diǎn)擊其中一個(gè)按鈕時(shí),會(huì)發(fā)出 AuthenticationEventLogin 事件以及一些參數(shù)(通常會(huì)被用于驗(yàn)證處理)
    • 如果身份驗(yàn)證失敗锅很,顯示一條錯(cuò)誤消息(第 60 ~ 64 行)

好了其馏,沒啥別的事了,很簡單對不爆安?

小技巧

你肯定注意到了叛复,我把頁面包在了 WillPopScope 里面,這是因?yàn)樯矸蒡?yàn)證是必須的步驟扔仓,除非成功登錄(驗(yàn)證通過)褐奥,我不希望用戶使用 Android 設(shè)備提供的 Back 鍵來跳過驗(yàn)證訪問到其它頁面。

3.6.5. 中間跳轉(zhuǎn)頁面: DecisionPage

如前所述翘簇,我希望 App 根據(jù)用戶登錄狀態(tài)自動(dòng)跳轉(zhuǎn)到 AuthenticationPageHomePage

代碼及說明如下:

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

提示

為了詳細(xì)解釋下面的問題撬码,我們先回溯下 Flutter 處理 Pages(也就是 路由Route)的方式,即使用 Navigator 對象來管理 Routes版保,而 Navigator 對象創(chuàng)建了一個(gè) Overlay 對象呜笑;這個(gè) Overlay 其實(shí)是包含多個(gè) OverlayEntryStack 對象,而每個(gè) OverlayEntry 都包含了一個(gè) Page彻犁;

當(dāng)我們通過 Navigator.of(context) 操作路由堆棧進(jìn)行壓入叫胁、彈出或替換時(shí),也會(huì)更新 Overlay 對象(也就是Stack 對象)汞幢,換句話說驼鹅,這些操作會(huì)導(dǎo)致 Stack 對象的重構(gòu);而 Stack 重構(gòu)時(shí)森篷,OverlayEntry (包括其內(nèi)容 Page)也會(huì)跟著重構(gòu)输钩;

結(jié)果就是:

當(dāng)我們通過 Navigator.of(context) 進(jìn)行路由操作后,所有其它頁面都會(huì)重構(gòu)仲智!

  • 那么买乃,為啥我要把它實(shí)現(xiàn)為 StatefulWidget ?

    為了能夠響應(yīng) AuthenticationState 任何變更,這個(gè) page 需要在 App 整個(gè)生命周期內(nèi)保留钓辆;

而根據(jù)上面的提示为牍,每次調(diào)用 Navigator.of(context) 后,這個(gè)頁面都會(huì)被重構(gòu)岩馍,因此也會(huì)重構(gòu) BlocEventStateBuilder ,毫無疑問 BlocEventStateBuilder 里面的 builder 方法也會(huì)被調(diào)用抖韩;

因?yàn)檫@個(gè) builder 方法是負(fù)責(zé)將用戶重定向到與 AuthenticationState 對應(yīng)的頁面蛀恩,重定向又要通過 Navigator.of(context) 來實(shí)現(xiàn)…明顯死循環(huán)了

所以為了防止這種情況發(fā)生,我們需要將「最后一個(gè)」 AuthenticationState 存起來茂浮,只有當(dāng)新的 AuthenticationState 與已存的不一樣時(shí)双谆,我們才進(jìn)行重定向處理壳咕;

而實(shí)現(xiàn)存儲(chǔ)就是利用 StatefulWidget 的特性,將「最后一個(gè)」 AuthenticationState 放到了 StateoldAuthenticationState 屬性中顽馋。

  • 到底是怎么運(yùn)作的谓厘?

    如上所訴,每當(dāng) AuthenticationState 被拋出時(shí)寸谜,BlocEventStateBuilder 會(huì)調(diào)用 builder 方法竟稳,根據(jù) isAuthenticated 標(biāo)識(shí),我們就知道具體將用戶重定向到哪個(gè)頁面熊痴。

小技巧

由于在 builder 中無法直接跳轉(zhuǎn)到其它界面他爸,我們使用了WidgetsBinding.instance.addPostFrameCallback() 方法來請求 Flutter 在完成渲染后執(zhí)行跳轉(zhuǎn)。

此外果善,除了 DecisionPage 需要在整個(gè)應(yīng)用生命周期保留之外诊笤,我們需要移除路由堆棧中重定向前所有其它已存在的頁面,所以我們使用了 Navigator.of(context).pushAndRemoveUntil(…) 來實(shí)現(xiàn)這一目的巾陕。參考 pushAndRemoveUntil()


3.6.6. 用戶注銷

為了讓用戶能夠注銷讨跟,可以創(chuàng)建一個(gè) LogOutButton,放到 App 中任何地方鄙煤。

這個(gè)按鈕只需要點(diǎn)擊后發(fā)出 AuthenticationEventLogout() 事件晾匠,這個(gè)事件會(huì)觸發(fā)如下的自動(dòng)處理動(dòng)作:

  1. 事件由 AuthenticationBloc 進(jìn)行處理
  2. 處理后拋出一個(gè) AuthentiationState(isAuthenticated = false)
  3. 拋出的狀態(tài)將由DecisionPage 通過 BlocEventStateBuilder 進(jìn)行處理
  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. 注入 AuthenticationBloc

由于需要 AuthenticationBloc 在應(yīng)用中任何頁面都可用,所以我們將其注入為 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(),
      ),
    );
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末混聊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子乾巧,更是在濱河造成了極大的恐慌句喜,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沟于,死亡現(xiàn)場離奇詭異咳胃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)旷太,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門展懈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人供璧,你說我怎么就攤上這事存崖。” “怎么了睡毒?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵来惧,是天一觀的道長。 經(jīng)常有香客問我演顾,道長供搀,這世上最難降的妖魔是什么隅居? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮葛虐,結(jié)果婚禮上胎源,老公的妹妹穿的比我還像新娘。我一直安慰自己屿脐,他們只是感情好涕蚤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著摄悯,像睡著了一般赞季。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上奢驯,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天申钩,我揣著相機(jī)與錄音,去河邊找鬼瘪阁。 笑死撒遣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的管跺。 我是一名探鬼主播义黎,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼豁跑!你這毒婦竟也來了廉涕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤艇拍,失蹤者是張志新(化名)和其女友劉穎狐蜕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卸夕,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡层释,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了快集。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贡羔。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖个初,靈堂內(nèi)的尸體忽然破棺而出乖寒,到底是詐尸還是另有隱情,我是刑警寧澤院溺,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布宵统,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏马澈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一弄息、第九天 我趴在偏房一處隱蔽的房頂上張望痊班。 院中可真熱鬧,春花似錦摹量、人聲如沸涤伐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凝果。三九已至,卻和暖如春睦尽,著一層夾襖步出監(jiān)牢的瞬間器净,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工当凡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留山害,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓沿量,卻偏偏與公主長得像浪慌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子朴则,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容