該文已受權公衆號 「碼個蛋」,轉載請指明出處 講了那麼多的部件,這節我打算來點不太同樣的,可能會沒有部件那麼好理解,也多是我講的不夠簡單明瞭,總之繫好安全帶,咱們要準備開車了。react
在 dart
部分記得分享過 Stream
的文章連接,可是我知道大家確定沒幾個願意看的,因此這裏再提下。仍是得從源碼開始...由於源碼的註釋比較長,就不貼註釋了,能夠本身看,我這邊就提取一些關鍵信息。git
Stream
是 Dart
提供的一種數據流訂閱管理的"工具",感受有點像 Android
中的 EventBus
或者 RxBus
,Stream
能夠接收任何對象,包括是另一個 Stream
,接收的對象經過 StreamController
的 sink
進行添加,而後經過 StreamController
發送給 Stream
,經過 listen
進行監聽,listen
會返回一個 StreamSubscription
對象,StreamSubscription
能夠操做對數據流的監聽,例如 pause
,resume
,cancel
等。github
Stream
分兩種類型:安全
Single-subscription Stream
:單訂閱 stream,整個生命週期只容許有一個監聽,若是該監聽 cancel 了,也不能再添加另外一個監聽,並且只有當有監聽了,纔會發送數據,主要用於文件 IO
流的讀取等。Broadcast Stream
:廣播訂閱 stream,容許有多個監聽,當添加了監聽後,若是流中有數據存在就能夠監聽到數據,這種類型,不論是否有監聽,只要有數據就會發送,用於須要多個監聽的狀況。仍是看下例子會比較直觀markdown
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(),
);
}
}
複製代碼
看下控制檯的輸出:app
果真把全部的數據都打印出來了,前面有說過,單訂閱的 stream 只有當 listen
後纔會發送數據,不試試我仍是不相信的,咱們把 _sink.add
放到 listen
前面去執行,再看控制檯的打印結果。竟然真的是同樣的,Google 粑粑果真誠不欺我。接着試下 pause
,resume
方法,看下數據如何監聽,修改代碼less
_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 被堵住了,數據不繼續發送了。ide
接下來看下廣播訂閱 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
模式了。
說實話,如今 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
,因此我就講下和 BLoC
有點關係的部分吧。RxDart
須要經過引入插件的方式引入(rxdart: ^0.21.0
)
若是須要查看詳細的內容,我這裏提供幾篇文章連接
RxDart: Magical transformations of Streams
其實 RxDart 就是對 Stream 的進一步分裝,RxDart 提供了三種 Subject,其功能相似 Stream 中的單訂閱 stream 和 廣播 stream。
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
的功能。
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
功能。
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
進行了進一步的封裝,提供了更多更方便的數據轉換方法,並且鏈式的寫法真的很舒服,用過了就停不下來,具體的方法介紹能夠參考上面提供的連接。
說實話本身封裝 BLoC
來實現分離邏輯和界面,相對仍是有點難度的,這邊能夠經過第三方來實現,這邊推薦 Google 粑粑的庫,flutter_provide,看下官方對關鍵部件和靜態方法的介紹
Provide<T>
- Widget used to obtain values from aProviderNode
higher up in the widget tree and rebuild on change. TheProvide<T>
widget should only be used withStream
s orListenable
s. Equivalent toScopedModelDescendant
inScopedModel
Provide.value<T>
- Static method used to get a value from aProviderNode
using theBuildContext
. This will not rebuild on change. Similar to manually writing a static.of()
method for anInheritedWidget
.Provide.stream<T>
- Static method used to get aStream
from aProviderNode
. Only works if eitherT
is listenable, or if theProvider
comes from aStream
.Provider<T>
- A class that returns a typed value on demand. Stored in aProviderNode
to allow retrieval usingProvide
.ProviderNode
- The equivalent of theScopedModel
widget. ContainsProviders
which can be found as anInheritedWidget
.
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.
這篇概念性的比較多,可是等理解了之後,對於之後的開發仍是很是有利的。
最後代碼的地址仍是要的:
文章中涉及的代碼:demos
基於郭神 cool weather
接口的一個項目,實現 BLoC
模式,實現狀態管理:flutter_weather
一個課程(當時買了想看下代碼規範的,代碼更新會比較慢,雖然是跟着課上的一些寫代碼,可是仍是作了本身的修改,不少地方看着不舒服,而後就改爲本身的實現方式了):flutter_shop
若是對你有幫助的話,記得給個 Star,先謝過,你的承認就是支持我繼續寫下去的動力~