在 Dart 非同步的使用情境中,除了常用的 Future 以外,還有 Stream 也是常常被用到的一個元件,相對於 Future 來說,Stream 是一個比較難理解的元件,今天就來研究一下 Stream 是什麼,以及如何建立與使用。
什麼是 Stream
根據 Flutter 官方影片提到的,Stream 是非同步元件的 Iterator 形式。白話一點來說,可以想像成一個由多個 Future 組成的 List。 我們可以用 awiat 等待一個 Future 的結果,在 Iterator 形式中,可以用 await for 等待 Stream 中的每一個非同步結果。
void main() async {
// 等待一個 Future
await Future.delayed(Duration(seconds: 1),
() => print("Hello World!!"));
// 等待一個 Stream
await for(var value in helloWorld()) {
print(value);
}
}
Stream<String> helloWorld() async* {
yield "Hello";
yield "World";
yield "!!";
} 如何建立 Stream
- 使用 async* 方法如同上面範例中寫的,我們可以寫一個方法,回傳 Stream 並標註為 async*,這樣就能透過 yield 回傳想要的值。 c#
Stream<String> helloWorld() async* { yield "Hello"; yield "World"; yield "!!"; } - 使用 StreamController除了使用 async* 方法之外,還可以使用 StreamController 來建立 Stream,建立完後可以透過 StreamController 的 add() 發送 Event(也等同於使用 StreamController.sink.add())。 c#
StreamController<String> streamController = new StreamController<String>(); void helloWorld() { streamController.add("Hello"); streamController.add("World"); streamController.add("!!"); } - 從其他 Stream 轉換最後一個方法則是從現有的 Stream 中創建,比較常見的例子像是 map、where..等方法。 c#
void main() async { var firstChars = helloWorld().map<String>((data) => data[0]); await for (var value in firstChars) { print(value); } } Stream<String> helloWorld() async* { yield "Hello"; yield "World"; yield "!!"; }
監聽 Stream 並獲取 Event
在前面的例子中,我們使用了 await for 來等待 Stream 中的所有 Event 回來,這樣會使得程式碼卡在 await for 那邊。在實際應用中,更多是使用 listen() ,以非同步的方式取得 Stream 的值。
Stream<String> helloWorld() async* {
yield "Hello";
yield "World";
yield "!!";
}
void main() {
helloWorld().listen((event) {
print(event);
});
} 常見的 Stream 操作
除了 listen 之外,我們也可以透過 Stream 的各式各樣 API 來操作 Stream,例如
- 使用 map 把 Stream 中的每一個 Event 轉成其他物件
- 使用 where 把預期的 Event 從 Stream 中過濾出來
- 使用 distinct 忽略相同的 Event,值得注意的是,distinct 是比較當前的 Event 與 上一個 Event 是否一樣,如果一樣就忽略掉。Stream 還有其他許多類似陣列的操作,這邊就不特別列出來,有興趣的人可以參考 Stream API。
在 UI 中使用 Stream
跟 FutureBuilder 一樣,如果需要根據 Stream 回傳的 Event 來影響畫面的話,可以使用 Flutter 提供的 StreamBuilder。我們可以傳入一個 Stream 到 StreamBuilder 中,當新的 Event 進來時,就可以根據 Event 重新呼叫 builder 方法渲染畫面。
class HelloWorld extends StatelessWidget {
const HelloWorld({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<String>(
stream: helloWorld(),
builder: (context, snapShot) {
var text = snapShot.hasData ? snapShot.data! : "Loading";
return Text(text);
},
);
}
Stream<String> helloWorld() async* {
yield "Hello";
yield "World";
yield "!!";
}
}
無用小知識
Q: Stream 如果是 Future 的 List 版本,那 Stream.first 是否會回傳 Future? A: 答案:是,確實會回傳 Future,但實際上 Stream 並非真的是 Future 的陣列,first 是透過 Stream.listen 取得第一個 Event 並把它包裝成 Future,然後才回傳給呼叫端。