Flutter實踐:深刻 flutter 的狀態管理方式(2)——演化BloC

上篇文章中,我詳細介紹了 InheritedWidget 及 ScopedModel 實現原理與方法,有同窗說找不到源碼,其實上篇文章包括這篇文章裏的源碼都按步驟放在樣例代碼裏了,有同窗說有點懵,其實上一篇的概念過多並且自己我表達也不是很清晰,英文文檔中我也解釋的沒有徹底語義化,因此還請諒解,結合代碼你會有更好地理解html

這篇的重點我將放在 BloC 的實現上面,咱們已經知道 Strems 的概念,RXDart 是依賴 Streams 使用的輸入(Sink)和輸出(Stream)封裝而成的響應式庫,BloC 基於此即可以實時偵聽數據的變化而改變數據,而且,BloC 主要解決的問題就是他不會一刀切的更新整個狀態樹,它關注的是數據,通過一系列處理後獲得它而且只改變應用它的 widget。react

如何將 Stream 中的數據應用到 Widget?

咱們先來實踐一下如何在 widget 中使用數據。Flutter 提供了一個名爲 StreamBuilder 的 StatefulWidget。git

StreamBuilder 監聽 Stream,每當一些數據流出 Stream 時,它會自動重建,調用其構建器回調。github

StreamBuilder<T>(
    key: ...optional, the unique ID of this Widget...
    stream: ...the stream to listen to...
    initialData: ...any initial data, in case the stream would initially be empty...
    builder: (BuildContext context, AsyncSnapshot<T> snapshot){
        if (snapshot.hasData){
            return ...the Widget to be built based on snapshot.data
        }
        return ...the Widget to be built if no data is available
    },
)
複製代碼

如下示例使用 Stream 而不是 setState() 模擬默認的「計數器」應用程序:app

import 'dart:async';
import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
  final StreamController<int> _streamController = StreamController<int>();

  @override
  void dispose(){
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
        child: StreamBuilder<int>(
          stream: _streamController.stream,
          initialData: _counter,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          _streamController.sink.add(++_counter);
        },
      ),
    );
  }
}
複製代碼
  • 第24-30行:咱們監聽流,每次有一個新值流出這個流時,咱們用該值更新 Text;
  • 第35行:當咱們點擊 FloatingActionButton 時,咱們遞增計數器並經過接收器將其發送到 Stream; 偵聽它的 StreamBuilder 注入了該值相應到後重建並「刷新」計數器;
  • 咱們再也不須要 State,全部東西均可以經過 Stream 接受;
  • 這裏實現了至關大的優化,由於調用 setState() 方法會強制整個 Widget(和任何子組件)從新渲染。 而在這裏,只重建 StreamBuilder(固然還有其子組件);
  • 咱們仍須要使用 StatefulWidget 的惟一緣由,僅僅是由於咱們須要經過 dispose 方法第15行釋放StreamController;

實現真正的 BloC

是時候展示真正的計技術了,咱們依然將 BloC 用於默認的計數器應用中:less

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        title: 'Streams Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: BlocProvider<IncrementBloc>(
          bloc: IncrementBloc(),
          child: CounterPage(),
        ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
        child: StreamBuilder<int>(
          stream: bloc.outCounter,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          bloc.incrementCounter.add(null);
        },
      ),
    );
  }
}

class IncrementBloc implements BlocBase {
  int _counter;

  //
  // Stream to handle the counter
  //
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  //
  // Stream to handle the action on the counter
  //
  StreamController _actionController = StreamController();
  StreamSink get incrementCounter => _actionController.sink;

  //
  // Constructor
  //
  IncrementBloc(){
    _counter = 0;
    _actionController.stream
                     .listen(_handleLogic);
  }

  void dispose(){
    _actionController.close();
    _counterController.close();
  }

  void _handleLogic(data){
    _counter = _counter + 1;
    _inAdd.add(_counter);
  }
}
複製代碼

這是上篇文章的最後給打你們製造懸念的代碼?五臟俱全,基本已經實現了 BloC。async

結合上面的例子來分析 BloC 體現出來的優點:(建議先將這段代碼跑起來!)ide

一,BloC 實現了責任分離

你能夠看到 CounterPage(第21-45行),其中沒有任何業務邏輯。函數

它承擔的負責僅有:佈局

  • 顯示計數器,如今只在必要時更新
  • 提供一個按鈕,當按下時,請求執行動做

此外,整個業務邏輯集中在一個單獨的類「IncrementBloc」中。

若是如今,若是咱們須要更改業務邏輯,只需更新方法 _handleLogic(第77-80行)。 也許新的業務邏輯將要求作很是複雜的事情...... CounterPage 永遠與它無關!

二,可測試性

如今,測試業務邏輯也變得更加容易。

無需再經過用戶界面測試業務邏輯。 只須要測試 IncrementBloc 類。

三,任意組織布局

因爲使用了 Streams,您如今能夠獨立於業務邏輯組織布局。

你能夠從應用程序中的任何位置用任何操做:只需調用 .incrementCounter 接收器便可。

您能夠在任何頁面的任何位置顯示計數器,只需艦艇監聽 .outCounter 流。

四,減小 「build」 的數量

不用 setState()而是使用 StreamBuilder,從而大大減小了「構建」的數量,只減小了所需的數量。

這是性能上的巨提升!

只有一個約束...... BLoC的可訪問性

爲了達到各類目的,BLoC 須要可訪問。

有如下幾種方法能夠訪問它:

  • 經過全局單例的變量

    這種方式很容易實現,但不推薦。 此外,因爲 Dart 中沒有類析構函數,所以咱們永遠沒法正確釋放資源。

  • 做爲本地實例

    您能夠實例化 BLoC 的本地實例。 在某些狀況下,此解決方案徹底符合需求。 在這種狀況下,您應該始終考慮在 StatefulWidget 中初始化,以便您能夠利用 dispose() 方法來釋放它。

  • 由根組件提供 使其可訪問的最多見方式是經過根 Widget,將其實現爲 StatefulWidget。

    如下代碼給出了一個通用 BlocProvider 的示例:(這個例子牛逼!)

    // Generic Interface for all BLoCs
    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;
      }
    }
    複製代碼

    關於這段通用的 BlocProvider 仔細回味,你會發現其精妙之處!

    通用 BlocProvider 的一些解釋:

    首先,如何將其用做數據提供者?

    若是你看了上面BloC 計數器的示例代碼示例代碼,您將看到如下代碼行(第12-15行)

    home: BlocProvider<IncrementBloc>(
              bloc: IncrementBloc(),
              child: CounterPage(),
            ),
    複製代碼

    使用以上代碼,咱們實例化了一個想要處理 IncrementBloc 的新 BlocProvider,並將 CounterPage 呈現爲子組件。

    BlocProvider 開始的子組件的任何組件部分都將可以經過如下行訪問 IncrementBloc

    IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
    複製代碼

BLoC 的基本使用就介紹完了,全部實例代碼在這裏 ,我將每種狀態管理的方法分模塊放在裏面,選擇使用哪一種方式運行代碼便可。

BloC 其餘你必須知道的事情

能夠實現多個 BloC

在大型項目中,這是很是可取的。 給如下幾個建議:

  • (若是有任何業務邏輯)每頁頂部有一個BLoC,
  • 用一個 ApplicationBloc 來處理應用程序全部狀態
  • 每一個「足夠複雜的組件」都有相應的BLoC。

如下示例代碼在整個應用程序的頂部使用 ApplicationBloc,而後在 CounterPage 頂部使用 IncrementBloc。該示例還展現瞭如何使用兩個 Bloc:

void main() => runApp(
  BlocProvider<ApplicationBloc>(
    bloc: ApplicationBloc(),
    child: MyApp(),
  )
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    return MaterialApp(
      title: 'Streams Demo',
      home: BlocProvider<IncrementBloc>(
        bloc: IncrementBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    final IncrementBloc counterBloc = BlocProvider.of<IncrementBloc>(context);
    final ApplicationBloc appBloc = BlocProvider.of<ApplicationBloc>(context);
    
    ...
  }
}
複製代碼

爲什麼不用 InheritedWidget 來全局管理 BloC 的狀態

我爲此也整理了一個將 BLoC 結合 InheritedWidget 使用的示例:bloc_inherited(在 Vscode 打開這段代碼是 [close_sinks] 的警告的)

在不少與 BLoC 相關的文章中,您將看到 Provider 的實現實際上是一個 InheritedWidget

固然, 這是徹底能夠實現的,然而,

  • 一個 InheritedWidget 沒有提供任何 dispose 方法,記住,在再也不須要資源時老是釋放資源是一個很好的作法。
  • 固然,你也能夠將 InheritedWidget 包裝在另外一個 StatefulWidget 中,可是,乍樣使用 InheritedWidget 並無什麼便利之處!
  • 最後,若是不受控制,使用 InheritedWidget 常常會致使一些反作用(請參閱下面的 InheritedWidget 上的提醒)。

這 3 點解釋了我爲什麼將通用 BlocProvider 實現爲 StatefulWidget,這樣我就能夠釋放資源

Flutter沒法實例化泛型類型

不幸的是,Flutter 沒法實例化泛型類型,咱們必須將 BLoC 的實例傳遞給 BlocProvider。 爲了在每一個BLoC中強制執行 dispose() 方法,全部BLoC都必須實現 BlocBase 接口。

關於使用 InheritedWidget 的提醒

在使用 InheritedWidget 並經過 context.inheritFromWidgetOfExactType(...) 獲取指定類型最近的 Widget 時,每當InheritedWidget 的父級或者子佈局發生變化時,這個方法會自動將當前 「context」(= BuildContext)註冊到要重建的 widget 當中。

請注意,爲了徹底正確,我剛纔解釋的與 InheritedWidget 相關的問題只發生在咱們將 InheritedWidgetStatefulWidget 結合使用時。 當您只使用沒有 State 的 InheritedWidget 時,問題就不會發生。

總結

Flutter 狀態管理的這幾種模式一樣能夠適用於不少軟件開發中,而 BloC 模式最初的設想是實現容許獨立於平臺重用相同的代碼!所以多花時間學習這類模式即是軟件開發的根基。

個人建議是將實例代碼運行出來閱讀代碼,依靠文章理解!但願能幫助到你!

參考連接

這篇內容是我反覆看谷歌大會寫完的。

而且大量借鑑了 Reactive Programming - Streams - BLoC 這篇文章。

相關文章
相關標籤/搜索