Flutter 學習之旅(四十五) Flutter 狀態 flutter_bloc學習(一)

在實際項目中我覺得大家一般不會直接使用StreamBuilder 的這種模式的BLoC,而是直接使用框架,網上經常提到到的框架有scoped_model ,flutter_bloc,flutter_redux, privoder 這些框架,

scoped_model

這個在前面文章已經分析過了,非常的小巧,利用了Microtask 微任務隊列做的異步通信和Flutter中InheritedWidget 控件的特性,可用于全局變量的傳遞,如果只是局部變量的傳遞,那么直接利用Element 的結構樹的特性直接找到與祖先綁定widget,即可以獲取這個共享的數據,

我們今天就來學習一下flutter_bloc 這個框架,學會用不是目的,提升自身才是最大的收益 ,我們使用的版本是從官網上找的最新代碼

  flutter_bloc: ^6.0.6

我們先來看一下簡單的示例: 還是一個比較常見的計數器,這里我會根據官網的介紹,逐步的向高級用法延伸,所以前面的東西會比較簡單,但是不要認為他不重要,即使是簡單也是前后配合完成了事件的通信,簡單的才是基礎,非常重要

///監聽類,BLoC礦建的靜態全局變量,這里打印了onChange 方法,
class TsmCountObserve extends BlocObserver{
  @override
  void onChange(Cubit cubit, Change change) {
    printString('${cubit.runtimeType} '+"     "+'$change');
    super.onChange(cubit, change);
  }
}


/// 實例類 
class TsmCountCubit extends Cubit<int>{
  TsmCountCubit(state) : super(state);
  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}


class TsmFlutterBLoCPageBase extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    Bloc.observer = TsmCountObserve();
    return BlocProvider(
      create: (context)=>TsmCountCubit(0),
      child: TsmFlutterBLoCPage(),
    );
  }
}

class TsmFlutterBLoCPage extends StatefulWidget{
  @override
  State<StatefulWidget> createState() =>_TsmFlutterBLoCState();
}

class _TsmFlutterBLoCState extends State<TsmFlutterBLoCPage>{
  @override
  Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Flutter BLoC 學習'),
          centerTitle: true,
        ),
        body: BlocBuilder<TsmCountCubit,int>(
          builder: (con,count){
            return Container(
              child: Text(count.toString()),
              alignment: Alignment.center,
            );
          },
        ),
        floatingActionButton: FloatingActionButton(
          key: const Key('counterView_increment_floatingActionButton'),
          child: const Icon(Icons.add),
          onPressed: () => context.bloc<TsmCountCubit>().increment(),
        ),
      );
  }
}

使用flutter_bloc 框架的基礎流程

由于Observe 是全局靜態變量為了打印日志,可有可無,所以他不在使用flutter_bloc這個框架的基礎流程中,

1>創建Privoider

如果大家從網上看到的代碼的話,這個provider 一般包裹的是MaterialApp()這個類,這樣更能體現出他的數據傳遞的特性,由于我這邊demo涉及到關于bloc的代碼太多,不能讓每一個provider 都包裹MaterialApp,這里我讓他包裹了一個StatefulWidget 來演示數據的傳遞,
Privoider的入參比較簡單,一個事件源,一個child,實際還有一個lazy 這個屬性,用來控制懶加載的,

2>BlocBuilder<Event,State>();接收數據

BlocBuilder來包裹在數據改變時需要變更的控件,參數包含一個build 用來構建控件的,注意他這里也可以提供另外的一個Cubit<T> , 這里可以解釋為事件源可能會有多個,你可以指定接收哪個信號源的信息,那么默認情況下是接收根 Privoider 提供的數據源,還是 直接包裹他的數據源呢,這里我們留一下一個疑問,在下面介紹源碼的時候我們再根據源碼具體說明,

bloc 發送數據

這里面非常巧妙的使用了拓展方法,將這個bloc<T>() 方法添加到了buildContext 的方法里面,具體實現代碼非常簡單就一行

extension BlocProviderExtension on BuildContext {
  C bloc<C extends Cubit<Object>>() => BlocProvider.of<C>(this);
}

最簡單的實現方式已經完成了,雖然代碼寫起來非常簡單,但是這里面有幾個問題需要我們思考一下,

1.provider 既然可以包裹在MaterialApp()外層,肯定是使用了InheritedWidget,那么他是如何來存儲這么需要的這個Cubit<T>數據呢?

2..既然是bloc模式,那么StreamController 的close 是何時調用的,怎么調用的呢,

3.同時存在根Cubit<T>和父Cubit<T> 的情況下,在BLoCBuilder 默認情況下會主動獲取哪個呢,

我們先看第三個問題,由于flutter_bloc的代碼中引用了太多的provider的方法,所以這里面很多的方法和類都是使用Provider 這個包下面的,
我們先來看看provider包下面的這個方法,具體返回的是什么,

  static T of<T>(BuildContext context, {bool listen = true}) {
    /// 獲取element 
    final inheritedElement = _inheritedElementOf<T>(context);
    if (listen) {
    ///如果listen為true , 則簡歷這個context與inheritedWidget 之間的聯系
      context.dependOnInheritedElement(inheritedElement);
    }
    return inheritedElement.value;
  }

  static _InheritedProviderScopeElement<T> _inheritedElementOf<T>(BuildContext context, ) {
    _InheritedProviderScopeElement<T> inheritedElement;
  ///如果這個控件本身就是 InheritedWidget 
    if (context.widget is _InheritedProviderScope<T>) {
    ///遍歷這個控件的祖先Element,找到第一個就打斷,
      context.visitAncestorElements((parent) {
        inheritedElement = parent.getElementForInheritedWidgetOfExactType<
            _InheritedProviderScope<T>>() as _InheritedProviderScopeElement<T>;
        return false;
      });
    } else {
///通過這個_inheritedWidgets Map來獲取的數據,所以這里不是遍歷,而是直接根據類型來獲取,也就是說,如果存在相同類型的
///inheritedWidgets  則后面的會替換掉前面已經添加進去的類型,即找到最近一個
      inheritedElement = context.getElementForInheritedWidgetOfExactType<
          _InheritedProviderScope<T>>() as _InheritedProviderScopeElement<T>;
    }
    if (inheritedElement == null) {
      throw ProviderNotFoundException(T, context.widget.runtimeType);
    }
    return inheritedElement;
  }

在_inheritedElementOf()這個方法里面告訴我們的答案就是找到最近的那個,

我們再來看看第2個問題 StreamController 的 close 是何時調用的,怎么調用的呢,
說道這里我們就不得不重新提一個Element 的生命周期的問題,在面前Element章節有說過傳送門 ( http://www.lxweimin.com/p/592561041c86 )
StatefulElement的管理著StatefulState 的生命方法,說道這里如果對element 的方法有過了解的肯定可以想到,這個dispse(),會由element的哪個方法去調用,沒錯就是unmount() 方法

///StatefulElement  的 unmount() 方法
  @override
  void unmount() {
    super.unmount();
    _state.dispose();
    _state._element = null;
    _state = null;
  }

我在看源碼的過程中并沒有找到provider他這個包里面包裹InheritedWidget 方法,所以就試著看一下Element 的源碼,在他們unmount 方法中找到了dispose()這個方法的調用時機,
下面我們跟著源碼來走一遍他的流程,我只是看了一個大概,部分實現邏輯比較多,我也沒有具體看,

  BlocProvider({
    Key key,
    @required CreateBloc<T> create,
    Widget child,
    bool lazy,
  }) : this._(
          key: key,
          create: create,
          dispose: (_, bloc) => bloc?.close(),
          child: child,
          lazy: lazy,
        );


  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    return InheritedProvider<T>(
      create: _create,
      dispose: _dispose,
      child: child,
      lazy: lazy,
    );
  }

首先這個BlocProvider 在 初始化的時候會調用_()的這個方法,什么也沒有干,目的是為了封裝這個dispose()的方法,并在buildWithChild 方法中又調用了Provider 包中 InheritedProvider,將它初始化的變量傳遞了過去,

  InheritedProvider({
    Key key,
    Create<T> create,
    T update(BuildContext context, T value),
    UpdateShouldNotify<T> updateShouldNotify,
    void Function(T value) debugCheckInvalidValueType,
    StartListening<T> startListening,
    Dispose<T> dispose,
    this.builder,
    bool lazy,
    Widget child,
  })  : _lazy = lazy,
        _delegate = _CreateInheritedProvider(
          create: create,
          update: update,
          updateShouldNotify: updateShouldNotify,
          debugCheckInvalidValueType: debugCheckInvalidValueType,
          startListening: startListening,
          dispose: dispose,
        ),
        super(key: key, child: child);

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    assert(
      builder != null || child != null,
      '$runtimeType used outside of MultiProvider must specify a child',
    );
    return _InheritedProviderScope<T>(
      owner: this,
      child: builder != null
          ? Builder(
              builder: (context) => builder(context, child),
            )
          : child,
    );
  }



///這個  Delegate 類似StatefulWidget 
class _CreateInheritedProvider<T> extends _Delegate<T> {
  _CreateInheritedProvider({
    this.create,
    this.update,
    UpdateShouldNotify<T> updateShouldNotify,
    this.debugCheckInvalidValueType,
    this.startListening,
    this.dispose,
  })  : assert(create != null || update != null),
        _updateShouldNotify = updateShouldNotify;

  final Create<T> create;
  final T Function(BuildContext context, T value) update;
  final UpdateShouldNotify<T> _updateShouldNotify;
  final void Function(T value) debugCheckInvalidValueType;
  final StartListening<T> startListening;
  final Dispose<T> dispose;

  @override
  _CreateInheritedProviderState<T> createState() =>
      _CreateInheritedProviderState();
}

在這個InheritedProvider 方法中其實干的事情也不過,初始化了一個類似StatefulWidget的 delegate ,并在buildWithChild方法中創建了真正的保存數據的主角 _InheritedProviderScope

class _InheritedProviderScope<T> extends InheritedWidget {
  _InheritedProviderScope({
    this.owner,
    @required Widget child,
  }) : super(child: child);

  final InheritedProvider<T> owner;

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) {
    return false;
  }

  @override
  _InheritedProviderScopeElement<T> createElement() {
    return _InheritedProviderScopeElement<T>(this);
  }
}

class _InheritedProviderScopeElement<T> extends InheritedElement
    implements InheritedContext<T> {
  _InheritedProviderScopeElement(_InheritedProviderScope<T> widget)
      : super(widget);

  @override
  void unmount() {
    _delegateState.dispose();
    super.unmount();
  }
}

最后發現在_InheritedProviderScopeElement 的unmount() 調用了最初由BLoCProvider 提供的這個dispose方法,

State 中的dispose 的實質就是element 的unmount 方法的調用,只要這個unmount 方法執行了,有沒有這個state并不重要,這也就是為什么要說這個dispose 方法的原因

看完了上面的代碼,再來看第一個問題,就非常簡單了,我們再來看一下provider.of<T>()的這個方法,

  static T of<T>(BuildContext context, {bool listen = true}) {
    /// 獲取element 
    final inheritedElement = _inheritedElementOf<T>(context);
    if (listen) {
    ///如果listen為true , 則簡歷這個context與inheritedWidget 之間的聯系
      context.dependOnInheritedElement(inheritedElement);
    }
    return inheritedElement.value;
  }

最后獲取的是inheritedElement.value , 也就是_InheritedProviderScopeElement 的value ,這么這個value怎么來的呢,
看看下面_InheritedProviderScopeElement 的 performRebuild()這個方法

class _InheritedProviderScopeElement<T> extends InheritedElement
    implements InheritedContext<T> {
  _InheritedProviderScopeElement(_InheritedProviderScope<T> widget)
      : super(widget);

  ///共享的數據
  @override
  T get value => _delegateState.value;
  
  _DelegateState<T, _Delegate<T>> _delegateState;

  @override
  void performRebuild() {
    if (_firstBuild) {
      _firstBuild = false;
      _delegateState = widget.owner._delegate.createState()..element = this;
    }
    super.performRebuild();
  }
}

從上面看這個delegatestate.value 是由_InheritedProviderScope.createState()后提供的,我們再來看一下這個createState()方法

@override
  T get value {
    bool _debugPreviousIsInInheritedProviderCreate;
    bool _debugPreviousIsInInheritedProviderUpdate;
    if (!_didInitValue) {
      _didInitValue = true;
      if (delegate.create != null) {
        try {
          _value = delegate.create(element);
        } finally {
        }
      }
      if (delegate.update != null) {
        try {
          _value = delegate.update(element, _value);
        } finally {
        }
      }
    }

    element._isNotifyDependentsEnabled = false;
    _removeListener ??= delegate.startListening?.call(element, _value);
    element._isNotifyDependentsEnabled = true;
    assert(delegate.startListening == null || _removeListener != null);
    return _value;
  }

這個value的類型就是我們最開始由BLoCProvicer 傳遞的這個create的方法創建的,但是數據是否是我們最開始的數據,就看是否對他做了修改,

至此,flutter_bloc的基礎功能就完事了,但是flutter_bloc 的精髓并不是這些,大家可以看到,基礎功能大多數都是使用了provider的這個包來完成的,他只是做了少量的封裝,如果業務不是很復雜,到這里已經夠用了,剩下的我會在后續文章中繼續說

我學習flutter的整個過程都記錄在里面了
http://www.lxweimin.com/c/36554cb4c804

最后附上demo 地址

https://github.com/tsm19911014/tsm_flutter

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