Flutter實踐:深入 flutter 的狀態管理方式(2)——演化BloC
在上篇文章中,我詳細介紹了 InheritedWidget 及 ScopedModel 實現原理與方法,有同學說找不到原始碼,其實上篇文章包括這篇文章裡的原始碼都按步驟放在 樣例程式碼 裡了,有同學說有點懵,其實上一篇的概念過多而且本身我表達也不是很清晰,英文文件中我也解釋的沒有完全語義化,所以還請諒解, 結合程式碼你會有更好地理解 。
這篇的重點我將放在 BloC 的實現上面,我們已經知道 Strems 的概念,RXDart 是依賴 Streams 使用的輸入( Sink )和輸出( Stream )封裝而成的響應式庫,BloC 基於此便可以實時偵聽資料的變化而改變資料,並且,BloC 主要解決的問題就是他不會一刀切的更新整個狀態樹,它關注的是資料,經過一系列處理後得到它並且只改變應用它的 widget。

如何將 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 相關的問題只發生在我們將 InheritedWidget 與 StatefulWidget 結合使用時。 當您只使用沒有 State 的 InheritedWidget 時,問題就不會發生。