【Flutter 專題】87 初識狀態管理 Bloc (二)

阿策小和尚發表於2021-07-22

      小菜前兩天剛學習了基本的 Bloc 狀態管理,其中 UI 通過 setState() 方式更新資料,今天進一步瞭解進階版的 FlutterBloc 狀態管理;

FlutterBloc

      FlutterBloc 可以更便利的實現 Bloc,主要是為了與 Bloc 共同使用而構建的;同樣需要提前瞭解幾個概念;小菜繼續以上一節中的 Demo 進行擴充套件,新增了 Number 的遞增和遞減;

BlocBuilder

      BlocBuilder 小菜理解為 Bloc 構造器,主要用於構建 Widget 以響應新的狀態,相較於 StreamBuilder 更便捷;可替代小菜上一節使用的 setState()

const BlocBuilder({
    Key key,
    @required this.builder,
    B bloc,
    BlocBuilderCondition<S> condition,
})
複製程式碼

      分析原始碼可知,builder 用於相應狀態的 Widgetbloc 為當前提供的範圍僅限於單個 Widget 且無法通過父級 BlocProvider 和當前級訪問的 Bloc 時才使用;而 condition 為可選的過度細粒度,包括兩個引數,之前的狀態和當前的狀態,返回值為 Boolean 型別,true 為更新狀態重建 Widgetfalse 時不重新構建;

@override
Widget build(BuildContext context) {
  return BlocBuilder<NumberBloc, int>(
      bloc: _numBloc,
      condition: (previousState, state) {
        print('BlocPage.condition->$previousState==$state');
        return state <= 30 ? true : false;
      },
      builder: (context, count) {
        return Scaffold(
            appBar: AppBar(title: Text('Bloc Page')),
            body: Center(
                child: Column( mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                  Text('當 Number > 30 時,不進行變更', style: TextStyle(fontSize: 20.0, color: Colors.blue)),
                  SizedBox(height: 20.0),
                  Text('當前 Number = ${_numBloc.state}', style: TextStyle(fontSize: 20.0, color: Colors.blue))
                ])),
            floatingActionButton: Column(
                crossAxisAlignment: CrossAxisAlignment.end,
                mainAxisAlignment: MainAxisAlignment.end,
                children: <Widget>[
                  FloatingActionButton(
                      heroTag: 'addTag', child: Icon(Icons.add),
                      onPressed: () => _numBloc.add(NumberEvent.addEvent)),
                  SizedBox(height: 20.0),
                  FloatingActionButton(
                      heroTag: 'removeTag', child: Icon(Icons.remove),
                      onPressed: () => _numBloc.add(NumberEvent.removeEvent))
                ]));
      });
}
複製程式碼

BlocProvider

      BlocProviderBloc 的供應者,建立 Bloc 並供應給其子控制元件樹;

BlocProvider({
    Key key,
    @required Create<T> create,
    Widget child,
    bool lazy,
})
複製程式碼

      簡單瞭解原始碼可知,BlocProvider 通過 create 建立一個 Bloc;通過 child 設定用來響應狀態的變更的 Widgetlazy 為是否懶建立(延遲建立),小菜理解的為是否在使用時再進行建立,預設為 true

class _BlocPageState extends State<BlocPage> {

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
        create: (BuildContext context) => NumberBloc(),
        child: BlocBuilder<NumberBloc, int>(
            condition: (previousState, state) {
          print('BlocPage.condition->$previousState==$state');
          return state <= 30 ? true : false;
        }, builder: (context, count) {
          return Scaffold(
              appBar: AppBar(title: Text('Bloc Page')),
              body: Center(
                  child: Column( mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                    Text('當 Number > 30 時,不進行變更', style: TextStyle(fontSize: 20.0, color: Colors.blue)),
                    SizedBox(height: 20.0),
                    Text('當前 Number = ${BlocProvider.of<NumberBloc>(context).state}', style: TextStyle(fontSize: 20.0, color: Colors.blue))
                  ])),
              floatingActionButton: Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: <Widget>[
                    FloatingActionButton(
                        heroTag: 'addTag', child: Icon(Icons.add),
                        onPressed: () => BlocProvider.of<NumberBloc>(context).add(NumberEvent.addEvent)),
                    SizedBox(height: 20.0),
                    FloatingActionButton(
                        heroTag: 'removeTag', child: Icon(Icons.remove),
                        onPressed: () => BlocProvider.of<NumberBloc>(context).add(NumberEvent.removeEvent))
                  ]));
        }));
  }
}
複製程式碼

BlocListener

      BlocListenerBlocBuilder 應用有相似之處;其中 listener 用於監聽狀態變更,可在此做出相應的業務處理;

class BlocListener<B extends Bloc<dynamic, S>, S> extends BlocListenerBase<B, S> with BlocListenerSingleChildWidget {
  final Widget child;
  const BlocListener({
    Key key,
    @required BlocWidgetListener<S> listener,
    B bloc,
    BlocListenerCondition<S> condition,
    this.child,
  })
}
複製程式碼

      簡單分析原始碼可得:

  1. childBlocListener 提供的 Widget 用來響應狀態的變更;
  2. blocBlocBuilder 對應的 bloc 用法相同,如果省略了 bloc 引數,BlocListener 將使用 BlocProvider 和當前函式自動執行查詢 BuildContext
  3. condition 為可選的過度細粒度,包括兩個引數,之前的狀態和當前的狀態,返回值為 Boolean 型別,true 為進行 listener 的監聽,false 時過濾掉 listener 的監聽;此時的過濾與 BlocBuilder 中的 condition 過濾無關;
  4. listener 在每次狀態變更時呼叫,其中包括上下文環境和當前狀態兩個引數;
@override
Widget build(BuildContext context) {
  return BlocListener<NumberBloc, int>(
      bloc: _numBloc,
      listener: (context, count) {
        print('BlocPage.listene->$count');
      },
      condition: (previousState, state) {
        print('BlocPage.BlocListener.condition->$previousState==$state');
        return state <= 20 ? true : false;
      },
      child: BlocBuilder<NumberBloc, int>(
          bloc: _numBloc,
          condition: (previousState, state) {
            print('BlocPage.condition->$previousState==$state');
            return state <= 30 ? true : false;
          },
          builder: (context, count) {
            return Scaffold(
                appBar: AppBar(title: Text('Bloc Page')),
                body: Center(
                    child: Column( mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                      Text('當 Number > 20 時,BlocListener 過濾 listener 監聽,與 BlocBuilder 中過濾的狀態無關', style: TextStyle(fontSize: 20.0, color: Colors.red)),
                      SizedBox(height: 20.0),
                      Text('當 Number > 30 時,Number 不進行變更', style: TextStyle(fontSize: 20.0, color: Colors.green)),
                      SizedBox(height: 20.0),
                      Text('當前 Number = ${_numBloc.state}', style: TextStyle(fontSize: 20.0, color: Colors.blue))
                    ])),
                floatingActionButton: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
                    children: <Widget>[
                      FloatingActionButton(
                          heroTag: 'addTag', child: Icon(Icons.add),
                          onPressed: () => _numBloc.add(NumberEvent.addEvent)),
                      SizedBox(height: 20.0),
                      FloatingActionButton(
                          heroTag: 'removeTag', child: Icon(Icons.remove),
                          onPressed: () => _numBloc.add(NumberEvent.removeEvent))
                    ]));
          }));
}
複製程式碼

TestCode

      小菜在測試過程中遇到一些小問題,僅簡單記錄一下,以防忘記;

Q1: There are multiple heroes that share the same tag within a subtree.

      小菜在擴充套件上一節的 Demo 時,點選進入頁面時會黑屏,提示如下錯誤;

A1: 在 FloatingActionButton 中新增 heroTag 區分

      以前在學習 Hero Animation 時,在同一個 Page 頁面不能用兩個相同的 heroTag,小菜這次忽略了 FloatingActionButton 中也應用了 Hero 動畫,需要區分一下即可;

floatingActionButton: Column(
    crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
    children: <Widget>[
      FloatingActionButton(
          heroTag: 'addTag', child: Icon(Icons.add),
          onPressed: () => _numBloc.add(NumberEvent.addEvent)),
      FloatingActionButton(
          heroTag: 'removeTag', child: Icon(Icons.remove),
          onPressed: () => _numBloc.add(NumberEvent.removeEvent))
    ]));
複製程式碼

Q2: BlocProvider.of() called with a context that does not contain a Bloc of type ...

      小菜在剛開始嘗試 BlocProvider.of(context) 方式獲取 Bloc 時報如下錯誤;

A2: 在 build() 外建立或通過如下方式建立,並建議與 BlocBuilder 成對設定

// build() 方法外建立
NumberBloc _numBloc;
@override
void initState() {
  super.initState();
  _numBloc = NumberBloc();
}

// BlocProvider create() 建立
BlocProvider(
  create: (BuildContext context) => NumberBloc(),
  child: ....
);
複製程式碼

      小菜剛接觸 FlutterBloc 很多高階用法還沒涉及到,下一節會嘗試多種 Bloc 共同使用的場景,對各方面理解還不到位,如有錯誤請多多指導!

來源: 阿策小和尚

相關文章