【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】
Dart 非同步程式設計:串流
一個顯示來自串流資料的簡單 Flutter 應用程式
本文涵蓋了反應式程式設計的基礎之一:串流,它們是 Stream 類型的物件。
如果您已閱讀我們先前關於 Futures 的文章 ,您可能記得 每個 Future 代表一個單一值 (錯誤或資料),以非同步 方式傳遞。串流的工作方式類似,只是它不是單一物件,串流可以隨著時間傳遞零個或多個值和錯誤 。
本文最初發表於 2020 年 2 月 。此版本將包含的程式碼更新為可空安全性。
本文是基於《Flutter in Focus》影片系列《Dart 中的非同步程式設計》的第三篇文章。第一篇文章 Isolates and event loops 涵蓋了 Dart 支援背景工作的基礎知識。第二篇 Futures 討論了 Future 類別。
如果您喜歡透過觀看或聆聽來學習,本文中的所有內容都包含在以下影片中。
注意: 本文中的程式碼已更新,以反映影片於 2019 年 6 月 28 日發佈後發生的最佳實務和 Dart 語言的變化(包括可空安全性)。
如果您考慮單一值與相同類型的 迭代器 的關係,那就是 Future 與串流的關係:Future 代表具有單一響應的單一請求,而串流代表具有多個響應的單一請求。
就像 Futures 一樣,關鍵是在事先決定 1) 當資料準備就緒時要做什麼,2) 發生錯誤時要做什麼,以及 3) 串流完成時要做什麼。與 Futures 一樣,在這個過程中,Dart 事件迴圈仍然在運行。
串流與 Dart 事件迴圈一起工作。
事件迴圈阻塞 例如,如果您使用 File 類別的 openRead() 方法從檔案讀取資料,此方法會返回一個串流。
資料塊從磁碟讀取並到達事件迴圈。Dart 函式庫查看它們並說:「啊,有人正在等待這個」,將資料加入到串流中,然後將其發送到您的應用程式。
當另一段資料到達時——它就會進入並出來。基於計時器的串流和從網路通訊端串流資料也與事件迴圈一起工作,使用時鐘和網路事件。
事件迴圈對資料進行排序。
監聽串流 接下來要了解的是如何使用串流提供的資料。
假設您有一個類別,它提供一個串流,每秒發出一個新的整數(1、2、3、4、5…)。您可以使用 listen() 方法訂閱串流。唯一的必需參數是一個函數。
1 2 3 4 5 final myStream = NumberCreator().stream;final subscription = myStream.listen( (data) => print ('Data: $data ' ), );
每次串流發出新值時,都會呼叫該函數並列印該值:
1 2 3 4 5 Data: 1 Data: 2 Data: 3 Data: 4 ...
這就是 listen() 的工作原理。
重要: 預設情況下,串流設定為單一訂閱。它們會保留其值,直到有人訂閱,並且它們在其整個生命週期中只允許一個監聽器。如果您嘗試兩次監聽同一個串流,則會收到異常。
幸運的是,Dart 也提供廣播串流。您可以使用 asBroadcastStream() 方法從單一訂閱串流建立廣播串流。廣播串流的工作方式與單一訂閱串流相同,但它們可以有多個監聽器。
廣播串流的另一個區別: 如果在資料準備就緒時沒有人在監聽,則該資料會被丟棄。
1 2 3 4 5 6 7 8 9 final myStream = NumberCreator().stream;final subscription = myStream.listen( (data) => print ('Data: $data ' ), ); final subscription2 = myStream.listen( (data) => print ('Data again: $data ' ), );
讓我們回到第一個 listen() 呼叫,因為還有一些事情要討論。
如前所述,串流可以像 Futures 一樣產生錯誤。透過將 onError 函數加入到 listen() 呼叫中,您可以捕獲和處理任何錯誤。
還有一個 cancelOnError 屬性,預設情況下為 true,但可以設定為 false,以便即使在發生錯誤後也能保持訂閱繼續進行。
您可以新增 onDone 函數,以便在串流完成傳送資料時執行一些程式碼,例如檔案已完全讀取完畢時。
結合所有四個參數——onError、onDone、cancelOnError 和必需的參數 (onData)——您可以預先準備好應對任何情況。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 final myStream = NumberCreator().stream;final subscription = myStream.listen( (data) { print ('Data: $data ' ); }, onError: (err) { print ('Error!' ); }, cancelOnError: false , onDone: () { print ('Done!' ); }, );
提示: listen() 返回的物件本身有一些有用的方法。它被稱為 StreamSubscription,您可以使用它來暫停、繼續,甚至取消資料流。
1 2 3 4 5 final subscription = myStream.listen(...);subscription.pause(); subscription.resume(); subscription.cancel();
使用和操作串流 現在您已經知道如何使用 listen() 訂閱串流並接收資料事件,讓我們來談談讓串流真正酷炫的地方:操作它們。
一旦您在串流中獲得了資料,許多操作就會突然變得流暢而優雅。
讓我們回到之前的數字串流。
使用名為 map() 的方法,您可以從串流中獲取每個值,並動態地將其轉換為其他內容。給 map() 一個執行轉換的函數,它會返回一個新的串流,其類型與函數的返回值相符。
現在不再是整數串流,而是一個字串串流。在末尾呼叫一個 listen(),將 print() 函數傳遞給它,現在它會直接從串流中列印字串——非同步地,當它們到達時。
1 2 3 NumberCreator().stream .map((i) => 'String $i ' ) .listen(print );
1 2 3 4 5 String 1 String 2 String 3 String 4 */
您可以像這樣連結許多方法。例如,如果您只想列印偶數,則可以使用 where() 過濾串流。給它一個測試函數,該函數為每個元素返回一個布林值,它會返回一個新的串流,該串流只包含通過測試的值。
1 2 3 4 NumberCreator().stream .where((i) => i % 2 == 0 ) .map((i) => 'String $i ' ) .listen(print );
1 2 3 4 String 2 String 4 String 6 String 8
distinct() 方法是另一個好方法。對於使用 Redux store 的應用程式,該 store 會在 onChange 串流中發出新的應用程式狀態物件。
您可以使用 map() 將狀態物件串流轉換為應用程式一部分的視圖模型串流。然後,您可以使用 distinct() 方法獲取一個串流,該串流會過濾掉連續的相同值(以防 store 啟動一個不影響視圖模型中資料子集的更改)。
然後,您可以監聽並在獲得新的視圖模型時更新 UI。
1 2 3 4 myReduxStore.onChange .map((s) => MyViewModel(s)) .distinct() .listen( )
Dart 內建了其他方法,您可以使用它們來調整和修改您的串流。此外,當您準備好使用更進階的功能時,還有 Dart 團隊維護的 async 套件 ,可在 pub.dev 上獲得。它包含可以合併兩個串流、快取結果以及執行其他基於串流的魔術的類別。
嘗試使用 async 套件來獲得更多基於串流的魔術。
若要獲得更多串流魔術,請查看 stream_transform 套件 。
建立串流 最後,還有一個值得一提的更進階的主題是如何建立您自己的串流。
就像 Futures 一樣,大多數情況下,您將使用網路函式庫、檔案函式庫、狀態管理等為您建立的串流,但您可以使用 StreamController 建立自己的串流。
讓我們回到我們一直使用的 NumberCreator 範例。這是它的實際程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 class NumberCreator { NumberCreator() { Timer.periodic(const Duration (seconds: 1 ), (timer) { _controller.sink.add(_count); _count += 1 ; }); } final _controller = StreamController<int >(); var _count = 0 ; Stream<int > get stream => _controller.stream; }
如您所見,它會持續計數,並使用計時器每秒增加一次計數。然而,有趣的是串流控制器。
StreamController 從頭開始建立一個全新的串流,並讓您可以存取它的兩端。有串流端本身,資料到達的地方。(我們在本文中一直在使用這個。)
1 Stream<int > get stream => _controller.stream;
還有接收端,這是將新資料加入到串流的地方:
1 _controller.sink.add(_count);
NumberCreator 使用了這兩個。當計時器關閉時,它會將最新的計數加入到控制器的接收端,然後它會使用公共屬性公開控制器的串流,以便其他物件可以訂閱它。
現在我們已經介紹了建立、操作和監聽串流,讓我們來談談如何在 Flutter 中使用它們來構建 Widget。
如果您閱讀了先前關於 Futures 的文章,您可能記得 FutureBuilder。您給它一個 Future 和一個構建器方法,它會根據 Future 的狀態構建 Widget。
對於串流,有一個類似的 Widget 稱為 StreamBuilder。給它一個像 number creator 那樣的串流和一個構建器方法,它會在每次串流發出新值時重建其子 Widget。
1 2 3 4 5 6 7 StreamBuilder<String >( stream: NumberCreator().stream.map((i) => 'String $i ' ), builder: (context, snapshot) { throw UnimplementedError("Case not handled yet" ); }, );
snapshot 參數是一個 AsyncSnapshot,就像 FutureBuilder 一樣。
1 2 3 4 5 6 7 8 9 StreamBuilder<String >( stream: NumberCreator().stream.map((i) => 'String $i ' ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Text('No data yet.' ); } throw UnimplementedError("Case not handled yet" ); }, );
您可以檢查其 connectionState 屬性以查看串流是否尚未傳送任何資料,或者它是否已完全完成。
1 2 3 4 5 6 7 8 9 10 11 StreamBuilder<String >( stream: NumberCreator().stream.map((i) => 'String $i ' ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Text('No data yet.' ); } else if (snapshot.connectionState == ConnectionState.done){ return const Text('Done!' ); } throw UnimplementedError("Case not handled yet" ); }, );
您可以使用 hasError 屬性來處理資料值,並查看最新值是否為錯誤。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 StreamBuilder<String >( stream: NumberCreator().stream.map((i) => 'String $i ' ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Text('No data yet.' ); } else if (snapshot.connectionState == ConnectionState.done) { return const Text('Done!' ); } else if (snapshot.hasError) { return const Text('Error!' ); } else { return Text(snapshot.data ?? '' ); } }, );
最重要的是要確保您的構建器知道如何處理串流的所有可能狀態。一旦您掌握了這一點,它就可以對串流的任何操作做出反應。(有關更多資訊,包括您可以使用的 DartPad 實例,請參閱 StreamBuilder API 頁面 。)
總結 本文涵蓋了串流的含義、如何從串流中獲取值、操作這些值的方法,以及 StreamBuilder 如何幫助您在 Flutter 應用程式中使用串流值。
您可以從 Dart 和 Flutter 文件中了解更多關於串流的資訊:
敬請期待 本系列的更多文章。接下來我們將討論 async 和 await。這是 Dart 提供的兩個關鍵字,可幫助您保持非同步程式碼的簡潔性和可讀性。
同時,您可以在我們的 YouTube 頻道上觀看關於《Dart 中的非同步程式設計》的下一部影片系列 ,或者訪問我們的網站 以獲取更多關於 Dart 和 Flutter 的資訊。
Dart 非同步程式設計:串流 最初發佈於 Medium 上的 Dart ,人們在那裡透過醒目顯示和回應這個故事來繼續對話。