我們都知道,Flutter中Widget的狀態控制了UI的更新,比如最常見的StatefulWidget,通過調用setState({})
方法來刷新控件。那么其他類型的控件,比如StatelessWidget就不能更新狀態來嗎?答案當然是肯定可以的。前文已經介紹過幾種狀態管理
Stream
Stream
是
Dart
提供的一種數據流訂閱管理的"工具",感覺有點像
Android
中的
EventBus
或者
RxBus
,
Stream
可以接收任何對象,包括是另外一個
Stream
,接收的對象通過
StreamController
的
sink
進行添加,然后通過
StreamController
發送給
Stream
,通過
listen
進行監聽,
listen
會返回一個
StreamSubscription
對象,
StreamSubscription
可以操作對數據流的監聽,例如
pause
,
resume
,
cancel
等。
Stream
分兩種類型:
Single-subscription Stream
:單訂閱 stream,整個生命周期只允許有一個監聽,如果該監聽 cancel 了,也不能再添加另一個監聽,而且只有當有監聽了,才會發送數據,主要用於文件IO
流的讀取等。Broadcast Stream
:廣播訂閱 stream,允許有多個監聽,當添加了監聽后,如果流中有數據存在就可以監聽到數據,這種類型,不管是否有監聽,只要有數據就會發送,用於需要多個監聽的情況。
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(), ); } }
運行后看下控制台的輸出:
listen
后才會發送數據,不試試我還是不相信的,我們把
_sink.add
放到
listen
前面去執行,再看控制台的打印結果。居然真的是一樣的,Google 粑粑果然誠不欺我。
pause
,
resume
方法,看下數據如何監聽,修改代碼:
_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});
pause
方法后,stream 被堵住了,數據不繼續發送了。

接下來看下廣播訂閱 stream,對代碼做下修改:
StreamController _controller = StreamController.broadcast(); //廣播訂閱 stream Sink _sink; StreamSubscription _subscription; @override void initState() { super.initState(); _sink = _controller.sink; // _sink 用於添加數據 _sink.add('A'); _subscription = _controller.stream.listen((data) => print('Listener: $data')); // 添加數據,stream 會通過 `listen` 方法打印 _sink.add(11); _subscription.pause(); _sink.add(11.16); _subscription.resume(); _sink.add([1, 2, 3]); _sink.add({'a': 1, 'b': 2}); } @override void dispose() { super.dispose(); // 最后要釋放資源... _sink.close(); _controller.close(); _subscription.cancel(); }
我們再看下控制台的打印:
總結:
單訂閱 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
。
所以,StreamBuilder是Stream在UI方面的一種使用場景,通過它我們可以在非StatefulWidget中保存狀態,同時在狀態改變時及時地刷新UI。
StreamBuilder
StreamBuilder其實是一個StatefulWidget,它通過監聽Stream,發現有數據輸出時,自動重建,調用builder方法。前面提到了 stream 通過 listen
進行監聽數據的變化,Flutter
就為我們提供了這么個部件 StreamBuilder
專門用於監聽 stream 的變化,然后自動刷新重建。接着來看下源碼
StreamBuilder<T>( key: ...可選... stream: ...需要監聽的stream... initialData: ...初始數據,否則為空... builder: (BuildContext context, AsyncSnapshot<T> snapshot){ if (snapshot.hasData){ return ...基於snapshot.hasData返回的控件 } return ...沒有數據的時候返回的控件 }, )
下面是一個模仿官方自帶demo“計數器”的一個例子,使用了StreamBuilder,而不需要任何setState:
import 'package:flutter/material.dart'; import 'dart:async'; 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,每次值改變的時候,更新Text中的內容 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: (){ // 每次點擊按鈕,更加_counter的值,同時通過Sink將它發送給Stream; // 每注入一個值,都會引起StreamBuilder的監聽,StreamBuilder重建並刷新counter _streamController.sink.add(++_counter); }, ), ); } }
setState
是一個很大的改進,因為我們不需要強行重建整個控件和它的子控件,只需要重建我們希望重建的StreamBuilder(當然它的子控件也會被重建)。我們之所以依然使用StatefulWidget的唯一原因就是:
StreamController需要在控件dispose()的時候被釋放。
能不能完全拋棄StatefulWidget?BLoC了解下
setState
方法,那么下一步,我們試試把
StatefulWidget
替換成
StatelessWidget
吧,而且
官方也推薦使用 StatelessWidget
替換 StatefulWidget
,這里就需要提下
BLoC
模式了。
InheritedWidget
來實現的,但是
InheritedWidget
沒有提供
dispose
方法,那么就會存在
StreamController
不能及時銷毀等問題,所以,參考了一篇國外的文章,
Reactive Programming - Streams - BLoC 這里通過使用
StatefulWidget
來實現,當該部件銷毀的時候,可以在其
dispose
方法中及時銷毀
StreamController
,這里我還是先當個搬運工,搬下大佬為我們實現好的基類
import 'package:flutter/material.dart';
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 的值:
import '../widget/baseBloc.dart';
import 'dart:async';
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
中.
import 'package:flutter/material.dart'; import './widget/baseBloc.dart'; import './bloc/counter.dart'; void main() => runApp(MyApp()); class MyApp 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. 實現了狀態管理
多個Bloc的使用
- 每一個有業務邏輯的頁面的頂層都應該有自己的BLoC;
- 每一個“足夠復雜的組建(complex enough component)”都應該有相應的BLoC;
- 可以使用一個
ApplicationBloc
來處理整個App的狀態。
下面的例子展示了在整個App的頂層使用ApplicationBloc,在CounterPage的頂層使用IncrementBloc:
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); ... } }
一個實踐Demo
大佬構建了一個偽應用程序來展示如何使用所有這些概念。 完整的源代碼可以在Github上找到。