前言
對於剛接觸
Flutter
的同學來說,Stream
(流)是一個相對比較抽象,也相對比較難以理解的東西。準確的來說Stream
並不是Flutter
的特性,而是Dart
語言自身所帶庫。Stream
和Future
都位於dart:async
核心庫,是Dart中非同步操作的兩大高手。所以不僅僅可以用於Flutter
,而是可以用於任何Dart
語言上的實現。在我們剛開始學習Flutter的時候基本都是使用
StatefulWidget
和setState((){})
來重新整理介面的資料,當我熟練使用流之後就可以基本完全使用StatelessWidget
告別StatefulWidget
同樣達到資料重新整理效果。
Stream分類
流可以分為兩類:
-
單訂閱流(Single Subscription),這種流最多隻能有一個監聽器(listener)
-
多訂閱流(Broadcast),這種流可以有多個監聽器監聽(listener)
Stream建立方式
-
Stream.fromFuture 接收一個Future物件作為引數
Stream<String>.fromFuture(getData()); Future<String> getData() async{ await Future.delayed(Duration(seconds: 5)); return "返回一個Future物件"; } 複製程式碼
-
Stream.fromIterable 接收一個集合物件作為引數
Stream<String>.fromIterable(['A','B','C']); 複製程式碼
-
Stream.fromFutures 接收一個Future集合物件作為引數
Stream<String>.fromFutures([getData()]); 複製程式碼
-
Stream.periodic 接收一個 Duration物件作為引數
Duration interval = Duration(seconds: 1); Stream<int> stream = Stream<int>.periodic(interval); 複製程式碼
操作方式及使用
使用Stream .periodic 方式建立一個Stream物件
用於建立一個週期傳送事件的流,如下
///用於建立一個每隔一秒傳送一次無限事件的流,並列印出值
void _stream() async{
Duration interval = Duration(seconds: 1);
Stream<int> stream = Stream.periodic(interval, (data) => data);
await for(int i in stream ){
print(i);
}
}
//(data){return data;} 上面的這句是用來回來值,如果省略這句,列印的值都為null
複製程式碼
.
-
Stream.take(int count)
上面建立了一個無限每隔一秒傳送一次事件的流,如果我們想指定只傳送10個事件則,用take。下面就只會列印出0-9
void _stream() async{ Duration interval = Duration(seconds: 1); Stream<int> stream = Stream.periodic(interval, (data) => data); stream = stream.take(10); //指定傳送事件個數 await for(int i in stream ){ print(i); } } 複製程式碼
-
Stream.takeWhile
上面這種方式我們是隻制定了傳送事件的個數,如果我們也不知道傳送多少個事件,我們可以從返回的結果上做一個返回值的限制,上面結果也可以用以下方式實現
void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream.periodic(interval, (data) => data); // stream = stream.take(10); stream = stream.takeWhile((data) { return data < 10; }); await for (int i in stream) { print(i); } } 複製程式碼
-
Stream.skip(int count)
skip可以指定跳過前面的幾個事件,如下會跳過0和1,輸出 2-9;
void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream.periodic(interval, (data) => data); stream = stream.take(10); stream = stream.skip(2); await for (int i in stream) { print(i); } } 複製程式碼
-
Stream.skipWhile
可以指定跳過不傳送事件的指定條件,如下跳過0-4的輸出,輸出5-9
void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream.periodic(interval, (data) => data); stream = stream.take(10); stream = stream.skipWhile((data) => data<5); await for (int i in stream) { print(i); } } 複製程式碼
-
Stream.toList()
將流中所有的資料收集存放在List中,並返回 Future<List>物件,listData裡面 0-9
1.這個是一個非同步方法,要結果則需要使用await關鍵字
2.這個是等待Stream當流結束時,一次返回結果
void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream.periodic(interval, (data) => data); stream = stream.take(10); List<int> listData = await stream.toList(); for (int i in listData) { print(i); } } 複製程式碼
-
Stream. listen()
這是一種特定的可以用於監聽資料流的方式,和 forEach迴圈的效果一致,但是返回的是
StreamSubscription<T>
物件,如下也會輸出0-9,同時列印出 ”流已完成“看一下原始碼這種方式可以接收
StreamSubscription<T> listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError}); 複製程式碼
1.
onData
是接收到資料的處理,必須要實現的方法2.
onError
流發生錯誤時候的處理3.
onDone
流完成時候調取4.
cancelOnError
發生錯誤的時候是否立馬終止void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream.periodic(interval, (data) => data); stream = stream.take(10); stream.listen((data) { print(data); }, onError: (error) { print("流發生錯誤"); }, onDone: () { print("流已完成"); }, cancelOnError: false); } 複製程式碼
-
Stream. forEach()
這中操作和
listen()
的方式基本差不多,也是一種監聽流的方式,這只是監聽了onData
,下面程式碼也會輸出0-9void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream.periodic(interval, (data) => data); stream = stream.take(10); stream.forEach((data) { print(data); }); } 複製程式碼
-
Stream .length
用於獲取等待流中所有事件發射完成之後統計事件的總數量,下面程式碼會輸出 10
void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream.periodic(interval, (data) => data); stream = stream.take(10); var allEvents = await stream.length; print(allEvents); } 複製程式碼
-
Stream.where
在流中新增篩選條件,過濾掉一些不想要的資料,滿足條件返回true,不滿足條件返回false,如下我們篩選出流中大於5小於10的資料
void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream<int>.periodic(interval, (data) => data); stream = stream.where((data)=>data>5); stream = stream.where((data)=> data<10); await for(int i in stream){ print(i); } } 複製程式碼
-
stream.map
對流中的資料進行一些變換,以下是我對Stream的每個資料都加1
void _stream() async { Duration interval = Duration(seconds: 1); Stream<int> stream = Stream<int>.periodic(interval, (data) => data); stream = stream.map((data) => data + 1); await for (int i in stream) { print(i); } } 複製程式碼
-
Stream.expand
對流中的資料進行一個擴充套件,如下,會輸出1,1,2,2,3,3….
void _stream() async { Duration interval = Duration(seconds: 1); Stream stream = Stream.periodic(interval, (data) => data); stream = stream.expand((data)=>[data,data]); stream.listen((data)=>print(data),onError:(error)=> print("發生錯誤") ); } 複製程式碼
-
Stream.transform
如果我們在在流流轉的過程中需要進行一些轉換和控制我們則需要使用到transform,接收一個
StreamTransformer<S,T>,S表示轉換之前的型別,T表示轉換後的輸入型別,如下程式碼我們會接收到三組數字模擬輸入了三次密碼,並判斷真確的密碼,同時輸出密碼正確和密碼錯誤:
void _stream() async { var stream = Stream<int>.fromIterable([123456,234567,678901]); var st = StreamTransformer<int, String>.fromHandlers( handleData: (int data, sink) { if (data == 678901) { sink.add("密碼輸入正確,正在開鎖。。。"); } else { sink.add("密碼輸入錯誤..."); } }); stream.transform(st).listen((String data) => print(data), onError: (error) => print("發生錯誤")); } 複製程式碼
輸入如下結果
I/flutter (18980): 密碼輸入錯誤... I/flutter (18980): 密碼輸入錯誤... I/flutter (18980): 密碼輸入正確,正在開鎖。。。 複製程式碼
StreamController使用
介紹完了
Stream
的基本概念和基本用法,上面直接建立流的方式,對我們本身開發來說,用途不是很大,我們在實際的開發過程中,基本都是使用的StreamContoller
來建立流。通過原始碼我們可以知道Stream
的幾種構造方法,最終都是通過StreamController
進行了包裝。
建立StreamController物件及使用
- 構建單訂閱的
Streamcontroller
//StreamController裡面會建立一個Stream,我們實際操控的Stream
StreamController<String> streamController = StreamController();
streamController.stream.listen((data)=> print(data));
streamController.sink.add("aaa");
streamController.add("bbb");
streamController.add("ccc");
streamController.close();
//上面程式碼我們會輸出 aaa,bbb,ccc
複製程式碼
注意:如果我們給上面的程式碼再加一個listen會報如下異常,所以單訂閱流,只能有一個listen。一般情況下我們多數都是使用的單訂閱流,我們也可以將單訂閱流轉成多訂閱流。
-
構建多監聽器的
StreamController
有兩種方式1.直接建立多訂閱
Stream
StreamController<String> streamController = StreamController.broadcast(); streamController.stream.listen((data){ print(data); },onError: (error){ print(error.toString()); }); streamController.stream.listen((data) => print(data)); streamController.add("bbb"); //上面程式碼回輸出 bbb,bbb 複製程式碼
2.將單訂閱流轉成多訂閱流
StreamController<String> streamController = StreamController(); Stream stream =streamController.stream.asBroadcastStream(); stream.listen((data) => print(data)); stream.listen((data) => print(data)); streamController.sink.add("aaa"); streamController.close(); //上面程式碼會輸出 aaa,aaa 複製程式碼
注意:在流用完了之後記得關閉,呼叫streamController.close()
StreamBuilder使用
前面我把
Stream
的常用方式做了簡單的介紹和演示,我們怎麼結合Flutter使用呢?在Flutter裡面提供了一個Widget
叫StreamBuilder
。StreamBuilder
其實是個StatefulWidget
它一直記錄著流中最新的資料,當資料流發生變化時,會自動呼叫builder方法進行重建。
-
StreamBuilder的原始碼如下,需要接受一個流,我們可以傳入一個
StreamController
的Stream
const StreamBuilder({ Key key, this.initialData, Stream<T> stream, @required this.builder, }) : assert(builder != null), super(key: key, stream: stream); 複製程式碼
-
使用
StreamController
結合StreamBuider
對官方的計數器進行改進,取代setState重新整理頁面,程式碼如下class MyHomePage extends StatefulWidget { @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _count = 0; final StreamController<int> _streamController = StreamController(); @override Widget build(BuildContext context) { return Scaffold( body: Container( child: Center( child: StreamBuilder<int>( stream: _streamController.stream, builder: (BuildContext context, AsyncSnapshot snapshot) { return snapshot.data == null ? Text("0") : Text("${snapshot.data}"); }), ), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () { _streamController.sink.add(++_count); }), ); } @override void dispose() { _streamController.close(); super.dispose(); } } 複製程式碼
原始碼相關
從StreamController原始碼我們可以看出來裡面建立的得過程 預設建立一個_SyncStreamController,具體可以去讀取下原始碼,看看
StreamController
是怎麼建立Stream
和StreamSink
factory StreamController(
{void onListen(),
void onPause(),
void onResume(),
onCancel(),
bool sync: false}) {
return sync
? new _SyncStreamController<T>(onListen, onPause, onResume, onCancel)
: new _AsyncStreamController<T>(onListen, onPause, onResume, onCancel);
}
複製程式碼
注意:上面我既使用了
streamController.sink.add("aaa");
新增資料,也使用了streamController.add("bbb");
方式新增資料。實際效果是一樣的,檢視原始碼可知 sink如下,實際上sink是對StreamController
的一種包裝,最終都是調取的StreamController.add方法;
class _StreamSinkWrapper<T> implements StreamSink<T> {
final StreamController _target;
_StreamSinkWrapper(this._target);
void add(T data) {
_target.add(data);
}
void addError(Object error, [StackTrace stackTrace]) {
_target.addError(error, stackTrace);
}
Future close() => _target.close();
Future addStream(Stream<T> source) => _target.addStream(source);
Future get done => _target.done;
複製程式碼
總結
以上是我在開發過程中,對Flutter
中Stream
使用的一些簡單的認識和常用方法的總結,StreamBuilder
和 StreamController
結合使用,實現區域性重新整理效果(比喻一個頁面有多個介面,各自應該重新整理自己控制的UI的部分而不應該整個頁面重新整理,時候我們可以使用StreamController
和StreamBuilder
達到這種效果)如有不到之處或者有偏差的地方,望指正~