在上篇文章中,我詳細介紹了 InheritedWidget 及 ScopedModel 實現原理與方法,有同學說找不到源碼,其實上篇文章包括這篇文章裏的源碼都按步驟放在樣例代碼裏了,有同學說有點懵,其實上一篇的概念過多而且本身我表達也不是很清晰,英文文檔中我也解釋的沒有完全語義化,所以還請諒解,結合代碼你會有更好地理解。
這篇的重點我將放在 BloC 的實現上面,我們已經知道 Strems 的概念,RXDart 是依賴 Streams 使用的輸入(Sink)和輸出(Stream)封裝而成的響應式庫,BloC 基於此便可以實時偵聽數據的變化而改變數據,並且,BloC 主要解決的問題就是他不會一刀切的更新整個狀態樹,它關注的是數據,經過一系列處理後得到它並且只改變應用它的 widget。
[圖片上傳失敗...(image-42614a-1549099616513)]
如何將 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 時,問題就不會發生。
總結
Flutter 狀態管理的這幾種模式同樣可以適用於很多軟件開發中,而 BloC 模式最初的設想是實現允許獨立於平臺重用相同的代碼!因此多花時間學習這類模式便是軟件開發的根基。
我的建議是將實例代碼運行出來閱讀代碼,依靠文章理解!希望能幫助到你!
參考鏈接
這篇內容是我反覆看完 Build reactive mobile apps with Flutter (Google I/O '18) 谷歌大會寫完的。
並且大量借鑑了 Reactive Programming - Streams - BLoC 這篇文章。