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

Meandni發表於2019-02-02

上篇文章中,我詳細介紹了 InheritedWidget 及 ScopedModel 實現原理與方法,有同學說找不到原始碼,其實上篇文章包括這篇文章裡的原始碼都按步驟放在樣例程式碼裡了,有同學說有點懵,其實上一篇的概念過多而且本身我表達也不是很清晰,英文文件中我也解釋的沒有完全語義化,所以還請諒解,結合程式碼你會有更好地理解

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

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

如何將 Stream 中的資料應用到 Widget?

我們先來實踐一下如何在 widget 中使用資料。Flutter 提供了一個名為 StreamBuilder 的 StatefulWidget。

StreamBuilder 監聽 Stream,每當一些資料流出 Stream 時,它會自動重建,呼叫其構建器回撥。

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() 模擬預設的“計數器”應用程式:

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 用於預設的計數器應用中:

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。

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

一,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 這篇文章。

相關文章