天天看點

Flutter -- 如何實作狀态管理,BLoC 一展身手

Stream

dart

部分記得分享過

Stream

的文章連結,但是我知道你們肯定沒幾個願意看的,是以這裡再提下。還是得從源碼開始...因為源碼的注釋比較長,就不貼注釋了,可以自己看,我這邊就提取一些關鍵資訊。

Stream

Dart

提供的一種資料流訂閱管理的"工具",感覺有點像

Android

中的

EventBus

或者

RxBus

Stream

可以接收任何對象,包括是另外一個

Stream

,接收的對象通過

StreamController

sink

進行添加,然後通過

StreamController

發送給

Stream

,通過

listen

進行監聽,

listen

會傳回一個

StreamSubscription

對象,

StreamSubscription

可以操作對資料流的監聽,例如

pause

resume

cancel

等。

Stream

分兩種類型:

  1. Single-subscription Stream

    :單訂閱 stream,整個生命周期隻允許有一個監聽,如果該監聽 cancel 了,也不能再添加另一個監聽,而且隻有當有監聽了,才會發送資料,主要用于檔案

    IO

    流的讀取等。
  2. Broadcast Stream

    :廣播訂閱 stream,允許有多個監聽,當添加了監聽後,如果流中有資料存在就可以監聽到資料,這種類型,不管是否有監聽,隻要有資料就會發送,用于需要多個監聽的情況。

還是看下例子會比較直覺

class _StreamHomeState extends State<StreamHome> {
  StreamController _controller = StreamController();  // 建立單訂閱類型 `StreamController`
  Sink _sink;
  StreamSubscription _subscription;

  @override
  void initState() {
    super.initState();

    _sink = _controller.sink; // _sink 用于添加資料
    // _controller.stream 會傳回一個單訂閱 stream,
    // 通過 listen 傳回 StreamSubscription,用于操作流的監聽操作
    _subscription = _controller.stream.listen((data) => print('Listener: $data'));

    // 添加資料,stream 會通過 `listen` 方法列印
    _sink.add('A');
    _sink.add(11);
    _sink.add(11.16);
    _sink.add([1, 2, 3]);
    _sink.add({'a': 1, 'b': 2});
  }

  @override
  void dispose() {
    super.dispose();
    // 最後要釋放資源...
    _sink.close();
    _controller.close();
    _subscription.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(),
    );
  }
}           

看下控制台的輸出:

果然把所有的資料都列印出來了,前面有說過,單訂閱的 stream 隻有當

listen

後才會發送資料,不試試我還是不相信的,我們把

_sink.add

放到

listen

前面去執行,再看控制台的列印結果。居然真的是一樣的,Google 粑粑果然誠不欺我。接着試下

pause

resume

方法,看下資料如何監聽,修改代碼

_sink = _controller.sink;
_subscription = _controller.stream.listen((data) => print('Listener: $data'));
_sink.add('A');
_subscription.pause(); // 暫停監聽
_sink.add(11);
_sink.add(11.16);
_subscription.resume(); // 恢複監聽
_sink.add([1, 2, 3]);
_sink.add({'a': 1, 'b': 2});           

再看控制台的列印,你們可以先猜下是什麼結果,我猜大部分人都會覺得應該是不會有 11 和 11.16 列印出來了。然鵝事實并非這樣,列印的結果并未發生變化,也就是說,調用

pause

方法後,stream 被堵住了,資料不繼續發送了。

接下來看下廣播訂閱 stream,對代碼做下修改

StreamController _controller = StreamController.broadcast();
  Sink _sink;
  StreamSubscription _subscription;

  @override
  void initState() {
    super.initState();

    _sink = _controller.sink;

    _sink.add('A');
    _subscription = _controller.stream.listen((data) => print('Listener: $data'));

    _sink.add(11);
    _subscription.pause();
    _sink.add(11.16);
    _subscription.resume();

    _sink.add([1, 2, 3]);
    _sink.add({'a': 1, 'b': 2});
  }
// ...
}           

我們再看下控制台的列印:

你猜對答案了嗎,這邊做下小總結:

單訂閱 Stream 隻有當存在監聽的時候,才發送資料,廣播訂閱 Stream 則不考慮這點,有資料就發送;當監聽調用 pause 以後,不管哪種類型的 stream 都會停止發送資料,當 resume 之後,把前面存着的資料都發送出去。

sink 可以接受任何類型的資料,也可以通過泛型對傳入的資料進行限制,比如我們對

StreamController

進行類型指定

StreamController<int> _controller = StreamController.broadcast();

因為沒有對

Sink

的類型進行限制,還是可以添加除了

int

外的類型參數,但是運作的時候就會報錯,

_controller

對你傳入的參數做了類型判定,拒絕進入。

Stream

中還提供了很多

StremTransformer

,用于對監聽到的資料進行處理,比如我們發送 0~19 的 20 個資料,隻接受大于 10 的前 5 個資料,那麼可以對 stream 如下操作

_subscription = _controller.stream
    .where((value) => value > 10)
    .take(5)
    .listen((data) => print('Listen: $data'));

List.generate(20, (index) => _sink.add(index));           

那麼列印出來的資料如下圖

除了

where

take

還有很多

Transformer

, 例如

map

skip

等等,小夥伴們可以自行研究。了解了

Stream

的基本屬性後,就可以繼續往下了~

StreamBuilder

前面提到了 stream 通過

listen

進行監聽資料的變化,

Flutter

就為我們提供了這麼個部件

StreamBuilder

專門用于監聽 stream 的變化,然後自動重新整理重建。接着來看下源碼

const StreamBuilder({
    Key key,
    this.initialData, // 初始資料,不傳入則為 null
    Stream<T> stream,
    @required this.builder
  }) : assert(builder != null),
       super(key: key, stream: stream);

@override
AsyncSnapshot<T> initial() => AsyncSnapshot<T>.withData(ConnectionState.none, initialData);           

StreamBuilder

必須傳入一個

AsyncWidgetBuilder

參數,初始值

initialData

可為空,

stream

用于監聽資料變化,

initial

方法的調用在其父類

StremBuilderBase

中,接着看下

StreamBuilderBaseState

的源碼,這裡我删除一些不必要的源碼,友善檢視,完整的源碼可自行檢視

class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>> {
  // ...
  @override
  void initState() {
    super.initState();
    _summary = widget.initial(); // 通過傳入的初始值生成預設值,如果沒有傳入則會是 null
    _subscribe(); // 注冊傳入的 stream,用于監聽變化
  }
  
  // _summary 為監聽到的資料
  @override
  Widget build(BuildContext context) => widget.build(context, _summary);

  // ...
  void _subscribe() {
    if (widget.stream != null) { 
      // stream 通過外部傳入,對資料的變化進行監聽,
      // 在不同回調中,通過 setState 進行更新 _summary
      // 當 _summary 更新後,由于調用了 setState,重新調用 build 方法,将最新的 _summary 傳遞出去
      _subscription = widget.stream.listen((T data) {
        setState(() { 
          _summary = widget.afterData(_summary, data); 
        });
      }, onError: (Object error) {
        setState(() {
          _summary = widget.afterError(_summary, error);
        });
      }, onDone: () {
        setState(() {
          _summary = widget.afterDone(_summary);
        });
      });
      _summary = widget.afterConnected(_summary); // 
    }
  }
}           

在之前更新資料都需要通過

setState

進行更新,這裡了解完了

stream

,我們就不使用

setState

更新,使用

Stream

來更新

class _StreamHomeState extends State<StreamHome> {
  // 定義一個全局的 `StreamController`
  StreamController<int> _controller = StreamController.broadcast();
  // `sink` 用于傳入新的資料
  Sink<int> _sink;
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _sink = _controller.sink;
  }

  @override
  void dispose() {
    super.dispose();
    // 需要銷毀資源
    _sink.close();
    _controller.close();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
          child: Container(
        alignment: Alignment.center,
        child: StreamBuilder(
          builder: (_, snapshot) => Text('${snapshot.data}', style: TextStyle(fontSize: 24.0)),
          stream: _controller.stream, // stream 在 StreamBuilder 銷毀的時候會自動銷毀
          initialData: _counter,
        ),
      )),
      // 通過 `sink` 傳入新的資料,去通知 `stream` 更新到 builder 中
      floatingActionButton: FloatingActionButton(
        onPressed: () => _sink.add(_counter++),
        child: Icon(Icons.add),
      ),
    );
  }
}           

那麼當點選按鈕的時候,就會重新整理界面上的值,通過上面的源碼分析,

StreamBuilder

也是通過

setState

方法進行重新整理,那麼兩種方法孰優孰劣呢,當然是通過

Stream

啦,這不是廢話嗎。因為通過調用

setState

重新整理的話,會把整個界面都進行重構,但是通過

StreamBuilder

的話,隻重新整理其

builder

,這樣效率就更高了,最後看小效果吧,所謂有圖有真相嘛

這一步,我們摒棄了

setState

方法,那麼下一步,我們試試把

StatefulWidget

替換成

StatelessWidget

吧,而且官方也推薦使用

StatelessWidget

替換

StatefulWidget

,這裡就需要提下

BLoC

模式了。

BLoC

說實話,現在 Google 下 「flutter bloc」能搜到很多文章,基本上都是通過

InheritedWidget

來實作的,例如這篇

Flutter | 狀态管理探索篇——BLoC(三)

,但是

InheritedWidget

沒有提供

dispose

方法,那麼就會存在

StreamController

不能及時銷毀等問題,是以,參考了一篇國外的文章,

Reactive Programming - Streams - BLoC

這裡通過使用

StatefulWidget

來實作,當該部件銷毀的時候,可以在其

dispose

方法中及時銷毀

StreamController

,這裡我還是先當個搬運工,搬下大佬為我們實作好的基類

abstract class BaseBloc {
  void dispose(); // 該方法用于及時銷毀資源
}

class BlocProvider<T extends BaseBloc> extends StatefulWidget {
  final Widget child; // 這個 `widget` 在 stream 接收到通知的時候重新整理
  final T bloc; 
  
  BlocProvider({Key key, @required this.child, @required this.bloc}) : super(key: key);

  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();

  // 該方法用于傳回 Bloc 執行個體
  static T of<T extends BaseBloc>(BuildContext context) {
    final type = _typeOf<BlocProvider<T>>(); // 擷取目前 Bloc 的類型
    // 通過類型擷取相應的 Provider,再通過 Provider 擷取 bloc 執行個體
    BlocProvider<T> provider = context.ancestorWidgetOfExactType(type); 
    return provider.bloc; 
  }

  static Type _typeOf<T>() => T;
}

class _BlocProviderState<T> extends State<BlocProvider<BaseBloc>> {
    
  @override
  void dispose() {
    widget.bloc.dispose(); // 及時銷毀資源
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}           

接着我們對前面的例子使用

BLoC

進行修改。

首先,我們需要建立一個

Bloc

類,用于修改 count 的值

class CounterBloc extends BaseBloc {
  int _count = 0;
  int get count => _count;

  // stream
  StreamController<int> _countController = StreamController.broadcast();

  Stream<int> get countStream => _countController.stream; // 用于 StreamBuilder 的 stream

  void dispatch(int value) {
    _count = value;
    _countController.sink.add(_count); // 用于通知修改值
  }

  @override
  void dispose() {
    _countController.close(); // 登出資源
  }
}           

在使用

Bloc

前,需要在最上層的容器中進行注冊,也就是

MaterialApp

void main() => runApp(StreamApp());

class StreamApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 這裡對建立的 bloc 類進行注冊,如果說有多個 bloc 類的話,可以通過 child 進行嵌套注冊即可
    // 放在最頂層,可以全局調用,當 App 關閉後,銷毀所有的 Bloc 資源,
    // 也可以在路由跳轉的時候進行注冊,至于在哪裡注冊,完全看需求
    // 例如實作主題色的切換,則需要在全局定義,當切換主題色的時候全局切換
    // 又比如隻有某個或者某幾個特殊界面調用,那麼完全可以通過在路由跳轉的時候注冊
    return BlocProvider(  
        child: MaterialApp(
          debugShowCheckedModeBanner: false,
          home: StreamHome(),
        ),
        bloc: CounterBloc());
  }
}

class StreamHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 擷取注冊的 bloc,必須先注冊,再去查找
    final CounterBloc _bloc = BlocProvider.of<CounterBloc>(context); 
    return Scaffold(
      body: SafeArea(
          child: Container(
        alignment: Alignment.center,
        child: StreamBuilder(
          initialData: _bloc.count,
          stream: _bloc.countStream,
          builder: (_, snapshot) => Text('${snapshot.data}', style: TextStyle(fontSize: 20.0)),
        ),
      )),
      floatingActionButton:
          // 通過 bloc 中的 dispatch 方法進行值的修改,通知 stream 重新整理界面
          FloatingActionButton(onPressed: () => 
                               _bloc.dispatch(_bloc.count + 1), child: Icon(Icons.add)),
    );
  }
}           

重新運作後,檢視效果還是一樣的。是以我們成功的對

StatefulWidget

進行了替換

再繼續講之前,先總結下

Bloc

**1. 成功的把頁面和邏輯分離開了,頁面隻展示資料,邏輯通過 BLoC 進行處理**

**2. 減少了 `setState` 方法的使用,提高了性能**

**3. 實作了狀态管理**
           

RxDart

因為上面的參考文章中提到了

RxDart

,個人覺得有必要了解下,當然目前也有很多文章介紹

RxDart

,是以我就講下和

BLoC

有點關系的部分吧。

RxDart

需要通過引入插件的方式引入(

rxdart: ^0.21.0

)

如果需要檢視詳細的内容,我這裡提供幾篇文章連結

RxDart 文檔 RxDart: Magical transformations of Streams

其實 RxDart 就是對 Stream 的進一步分裝,RxDart 提供了三種 Subject,其功能類似 Stream 中的單訂閱 stream 和 廣播 stream。

  1. PublishSubject

    /// PublishSubject is, by default, a broadcast (aka hot) controller, in order
    /// to fulfill the Rx Subject contract. This means the Subject's `stream` can
    /// be listened to multiple times.           
    通過注釋可以發現

    PuslishSubject

    不可被多次訂閱,盡管實作是通過

    StreamController<T>.broadcast

    方式實作,其實三種都是通過

    broadcast

    方式實作的,是以實作的功能就是類似

    Single-subscription Stream

    的功能。
  2. BehaviorSubject

    /// BehaviorSubject is, by default, a broadcast (aka hot) controller, in order
    /// to fulfill the Rx Subject contract. This means the Subject's `stream` can
    /// be listened to multiple times.           

    BehaviorSubject

    可以被多次訂閱,那麼這個就是實作了

    Broadcast Stream

    功能。
  3. ReplaySubject

    /// ReplaySubject is, by default, a broadcast (aka hot) controller, in order
    /// to fulfill the Rx Subject contract. This means the Subject's `stream` can
    /// be listened to multiple times.           

    ReplaySubject

    其實也是實作

    Broadcast Stream

    功能,那麼它和

    BehaviorSubject

    的差別在哪呢,别急,等我慢慢講。
    /// As items are added to the subject, the ReplaySubject will store them.
    /// When the stream is listened to, those recorded items will be emitted to
    /// the listener.           
    當有資料添加了,但是還沒有監聽的時候,它會将資料存儲下來,等到有監聽了,再發送出去,也就是說,

    ReplaySubject

    實作了

    Brodacast Stream

    的多訂閱功能,同時也實作了

    Single-subscription Stream

    的存儲資料的功能,每次添加了新的監聽,都能夠擷取到全部的資料。當然,這還不是它的全部功能,它還可以設定最大的監聽數量,會隻監聽最新的幾個資料,在注釋中,提供了這麼兩個例子,可以看下
    /// ### Example 
    ///
    ///     final subject = new ReplaySubject<int>();
    ///
    ///     subject.add(1);
    ///     subject.add(2);
    ///     subject.add(3);
    ///
    ///     subject.stream.listen(print); // prints 1, 2, 3
    ///     subject.stream.listen(print); // prints 1, 2, 3
    ///     subject.stream.listen(print); // prints 1, 2, 3
    ///
    /// ### Example with maxSize
    ///
    ///     final subject = new ReplaySubject<int>(maxSize: 2); // 實作監聽數量限制
    ///
    ///     subject.add(1);
    ///     subject.add(2);
    ///     subject.add(3);
    ///
    ///     subject.stream.listen(print); // prints 2, 3
    ///     subject.stream.listen(print); // prints 2, 3
    ///     subject.stream.listen(print); // prints 2, 3           

那麼我們可以使用

RxDart

對前面使用

Stream

實作的例子進行替換,最簡單的其實隻需要使用

BehaviorSubject

StreamController.broadcast()

就可以了,别的都不需要變化。但是

RxDart

有自己的變量,還是按照

RxDart

的方式來

// 繼承自 StreamController,是以 StreamController 擁有的屬性都有
BehaviorSubject<int> _countController = BehaviorSubject();
//  StreamController<int> _countController = StreamController.broadcast();

// 繼承自 Stream,是以這裡直接用之前 stream 的寫法也沒問題,但是這樣就有點不 RxDart 了
Observable<int> get countStream => Observable(_countController.stream);
//  Stream<int> get countStream => _countController.stream;

void dispatch(int value) {
  _count = value;
  // 直接提供了 add 方法,不需要通過 sink 來添加
  _countController.add(_count);
//    _countController.sink.add(_count);
}           

再次運作還是能過實作相同的效果。如果說要在

RxDart

Stream

兩種實作方式中選擇一種,個人更偏向于

RxDart

,因為它對

Stream

進行了進一步的封裝,提供了更多更友善的資料轉換方法,而且鍊式的寫法真的很舒服,用過了就停不下來,具體的方法介紹可以參考上面提供的連結。

Provide

說實話自己封裝

BLoC

來實作分離邏輯和界面,相對還是有點難度的,這邊可以通過第三方來實作,這邊推薦 Google 粑粑的庫,

flutter_provide

,看下官方對關鍵部件和靜态方法的介紹

  • Provide<T>

    - Widget used to obtain values from a

    ProviderNode

    higher up in the widget tree and rebuild on change. The

    Provide<T>

    widget should only be used with

    Stream

    s or

    Listenable

    s. Equivalent to

    ScopedModelDescendant

    in

    ScopedModel

  • Provide.value<T>

    - Static method used to get a value from a

    ProviderNode

    using the

    BuildContext

    . This will not rebuild on change. Similar to manually writing a static

    .of()

    method for an

    InheritedWidget

    .
  • Provide.stream<T>

    - Static method used to get a

    Stream

    from a

    ProviderNode

    . Only works if either

    T

    is listenable, or if the

    Provider

    comes from a

    Stream

  • Provider<T>

    - A class that returns a typed value on demand. Stored in a

    ProviderNode

    to allow retrieval using

    Provide

  • ProviderNode

    - The equivalent of the

    ScopedModel

    widget. Contains

    Providers

    which can be found as an

    InheritedWidget

Provide

這個部件主要用于從上層的

ProvideNode

中擷取值,當變化的時候重新整理重建,隻能同

Stream

Listenable

一同使用,類似于

ScopeMode

ScopedModelDescendant

。(這個部件放在需要狀态管理的部件的上層,例如有個

Text

需要修改狀态,那麼就需要在外層提供一個

Provide

部件,通過内部

builder

參數傳回

Text

部件)

Provide.value

是個靜态方法,用于從

ProvideNode

擷取值,但是當接收的值改變的時候不會重建。類似于

InheritedWidget

的靜态方法

of

。(這個方法用于擷取指定類型的 provide,每個 provide 都需要提供一個資料類,該類

with ChangeNotifier

,當資料變化的時候通過

notifyListeners

通知 provide 變化,進行重新整理重建)

Provide.stream

ProvideNode

擷取一個

stream

,僅在 T 可被監聽,或者 Provide 來自 stream 的情況下有效。(這個通常結合

StreamBuilder

使用,

StreamBuilder

在上面已經提到,就不多說了)

Provider

按需要的類型傳回相關值的類,存儲在

ProviderNode

中友善

Provide

進行檢索。(這個類主要是将我們自己建立的資料類通過

function

等方法轉換成

Provider

,并在

Providers

中進行注冊)

ProvideNode

類似于

ScopedModel

的一個部件,包含所有能被查找的

Providers

。(這個需要放在頂層,友善下面的容器進行查找 provider,重新整理相應的部件,一般放在

MaterialApp

上層)

這邊再補充一個個人覺得關鍵的類

Providers

,這個類主要用于存儲定義的

Provider

,主要是在建立

MaterialApp

的時候将需要用到的

Provider

通過

provide

方法添加進去存儲起來,然後在

ProvideNode

中注冊所有的

provider

友善下層容器擷取值,并調用。

說那麼多,還不如直接看個例子直接,代碼來了~,首先需要建立一個類似

BLoC

中監聽資料變化的

counter_bloc

類的資料管理類,我們這邊定義為

count_provider

需要混入

ChangeNotifier

class CountProvider with ChangeNotifier {
  int _value = 0; // 存儲的資料,也是我們需要管理的狀态值

  int get value => _value; // 擷取狀态值

  void changeValue(int value) {
    _value = value;
    notifyListeners(); // 當狀态值發生變化的時候,通過該方法重新整理重建部件
  }
}           

然後需要将定義的類注冊到全局的

Providers

void main() {
  final providers = Providers()
    // 将我們建立的資料管理類,通過 Provider.function 方法轉換成 Provider,
    // 然後添加到 Providers 中
    ..provide(Provider.function((_) => CountProvider()));
  // 在 App 上層,通過包裹一層 ProvideNode,并将我們生成的 Providers 執行個體
  // 注冊到 ProvideNode 中去,這樣整個 App 都可以通過 Provide.value 查找相關的 Provider
  // 找到 Provider 後就可以找到我們的資料管理類
  runApp(ProviderNode(child: StreamApp(), providers: providers));
}           

接着就是替換我們的界面實作了,前面通過

BLoC

實作,這裡替換成

Provide

來實作

class StreamHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
          child: Container(
        alignment: Alignment.center,
        // 通過指定類型,擷取特定的 Provide,這個 Provide 會傳回我們的資料管理類 provider
        // 通過内部定義的方法,擷取到需要展示的值
        child: Provide<CountProvider>(builder: (_, widget, provider) => Text('${provider.value}')),
      )),
      floatingActionButton: FloatingActionButton(
          onPressed: () =>
                // 通過 value 方法擷取到我們的資料管理類 provider,
                // 通過調用改變值的方法,修改内部的值,并通知界面重新整理重建
              Provide.value<CountProvider>(context).changeValue(
                  Provide.value<CountProvider>(context).value + 1),
          child: Icon(Icons.add))
    );
  }
}           

本文代碼檢視

bloc

包名下的所有檔案,需要單獨運作

stream_main.dart

檔案

最後運作後還是一樣的效果,也摒棄了

StatefulWidget

部件和

SetState

方法,實作了邏輯和界面分離。但是

Provide

最終還是通過

InheritedWidget

來實作,當然在資源方面 Google 的大佬們做了一些相關的處理,至于如何處理,這邊就不多說了。目前

provide

的這個庫還存在一點争議的地方,具體檢視

issue#3

,但是目前來看并沒有太大的影響。當然你不放心的話,可以使用

Scoped_model

或者上面的

Bloc

模式,Google 在文檔也有相關的注明

If you must choose a package today, it's safer to go with

package:scoped_model

than with this package.

這篇概念性的比較多,但是等了解了以後,對于以後的開發還是非常有利的。

上一篇: ECS初體驗

繼續閱讀