0%

【文章翻譯】Dart asynchronous programming: Streams

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

Dart 非同步程式設計:資料流

一個顯示來自資料流資料的簡單 Flutter 應用程式

本文涵蓋了響應式程式設計的基礎之一:資料流,它們是 Stream 類型的物件。

如果您已閱讀 上一篇關於 Future 的文章,您可能記得每個 Future 代表一個它非同步傳遞的單一值(錯誤或資料)。資料流的工作原理類似,但不同的是,一個資料流可以隨著時間推移傳遞零個或多個值和錯誤。

本文是基於「Flutter in Focus」影片系列「Dart 中的非同步程式設計」的第三篇文章。第一篇文章 Isolates and event loops 涵蓋了 Dart 對背景工作的支援的基礎知識。第二篇 Futures 討論了 Future 類別。

如果您喜歡透過觀看或聆聽來學習,本文中的所有內容都包含在以下影片中。

如果您考慮單個值與相同類型的 迭代器 的關係,那麼 Future 與資料流的關係也是如此。

就像 Future 一樣,關鍵是在事前決定:「當資料準備就緒時,出現錯誤時以及資料流完成時該怎麼做。」

同樣 就像 Future 一樣,Dart 事件迴圈仍然在運行。

資料流與 Dart 事件迴圈一起工作。

例如,如果您使用 File 類別的 openRead() 方法從檔案中讀取資料,則該方法會返回一個資料流。

資料塊從磁碟讀取並到達事件迴圈。 Dart 函式庫會查看它們並說:「啊,有人在等待這個」,將資料加到資料流中,然後它會彈出到您的應用程式程式碼中。

當另一條資料到達時,它就會進入並出來。基於計時器的資料流、從網路通訊端串流資料 - 它們也與事件迴圈一起工作,使用時鐘和網路事件。

監聽資料流

讓我們來談談如何使用資料流提供的資料。假設您有一個類別,它會提供一個資料流,每秒鐘產生一個新的整數:1、2、3、4、5……

您可以使用 listen() 方法訂閱資料流。唯一的必需參數是一個函式。

每次資料流發出一個新值時,都會調用該函式並列印該值:

1
2
3
4
5
Data: 1
Data: 2
Data: 3
Data: 4
...

這就是 listen() 的工作原理。

重要資訊: 預設情況下,資料流設定為單一訂閱。它們會保留其值,直到有人訂閱,並且它們在其整個生命週期中只允許一個監聽器。如果您嘗試兩次監聽同一個資料流,則會收到例外狀況。

幸好,Dart 也提供廣播資料流。您可以使用 asBroadcastStream() 方法從單一訂閱資料流建立廣播資料流。廣播資料流的工作方式與單一訂閱資料流相同,但它們可以有多個監聽器,如果在資料準備就緒時沒有人在監聽,則該資料會被丟棄。

讓我們回到第一個 listen() 呼叫,因為還有一些事情要談。

正如我們前面提到的,資料流可以像 Future 一樣產生錯誤。透過在 listen() 呼叫中加入 onError 函式,您可以捕獲和處理任何錯誤。

還有一個 cancelOnError 屬性,預設值為 true,但可以設定為 false,以便即使在發生錯誤後也能保持訂閱。

您可以加入一個 onDone 函式,在資料流完成傳送資料時執行一些程式碼,例如檔案已完全讀取時。

結合所有四個參數 - onErroronDonecancelOnError 和必需的參數 (onData) - 您可以提前為任何情況做好準備。

提示: listen() 返回的小型訂閱物件本身有一些有用的方法。它是一個 StreamSubscription,您可以使用它來暫停、恢復甚至取消資料流。

使用和操作資料流

現在您已經知道如何使用 listen() 訂閱資料流並接收資料事件,我們可以討論一下是什麼讓資料流如此酷炫:操作它們。一旦您在資料流中獲得了資料,許多操作就會變得流暢而優雅。

回到之前的數字資料流,我們可以使用一個稱為 map() 的方法來獲取資料流中的每個值,並即時將其轉換為其他內容。為 map() 提供一個執行轉換的函式,它會返回一個新的資料流,其類型與函式的返回值相符。您現在擁有一個字串資料流,而不是整數資料流。您可以在最後加上一個 listen() 呼叫,為它提供 print() 函式,現在您就可以直接從資料流中列印字串,非同步地,在它們到達時。

有很多方法可以像這樣串在一起。例如,如果您只想列印偶數,則可以使用 where() 過濾資料流。為它提供一個測試函式,該函式為每個元素返回一個布林值,它會返回一個新的資料流,其中僅包含通過測試的值。

distinct() 方法是另一個好方法。如果您有一個使用 Redux 存放區的應用程式,該存放區會在 onChange 資料流中發出新的應用程式狀態物件。您可以使用 map() 將該狀態物件資料流轉換為應用程式一部分的視圖模型資料流。然後,您可以使用 distinct() 方法獲取一個資料流,該資料流會濾除連續相同的值(以防存放區發出對視圖模型中的資料子集沒有影響的變更)。然後,您可以在每次獲得新的視圖模型時監聽並更新 UI。

Dart 內建了許多其他方法,您可以使用它們來調整和修改您的資料流。此外,當您準備好進行更進階的操作時,在 pub.dev 上可以找到由 Dart 團隊維護的 async 套件。它有一些類別可以合併兩個資料流、快取結果,並執行其他類型的基於資料流的魔法。

試試 async 套件,了解更多基於資料流的魔法。

更多資料流魔法,請查看 stream_transform 套件

建立資料流

這裡有一個進階主題值得一提,那就是如何建立自己的資料流。就像 Future 一樣,大多數時候您將使用網路函式庫、檔案函式庫、狀態管理等為您建立的資料流。但您也可以使用 StreamController 建立自己的資料流。

讓我們回到我們一直在使用的 NumberCreator。以下是它的實際程式碼:

如您所見,它會持續計數,並使用計時器每秒鐘增加計數。然而,有趣的是資料流控制器。

StreamController 從頭開始建立一個全新的資料流,並讓您可以存取它的兩端。有資料流本身的末端,資料到達的地方。(我們在整篇文章中一直在使用它。)

1
Stream<int> get stream => _controller.stream;

然後是接收端,這是新資料被加入到資料流的地方:

1
_controller.sink.add(_count);

這裡的 NumberCreator 使用了它們兩個。當計時器關閉時,它會將最新的計數加入到控制器的接收端,然後使用公開屬性公開控制器的資料流,以便其他物件可以訂閱它。

使用資料流構建 Flutter Widget

現在我們已經介紹了建立、操作和監聽資料流,讓我們來談談如何在 Flutter 中使用它們來構建 Widget。

如果您看過之前的關於 Future 的影片,您可能記得 FutureBuilder。您為它提供一個 Future 和一個構建器方法,它會根據 Future 的狀態構建 Widget。

對於資料流,有一個類似的 Widget 稱為 StreamBuilder。為它提供一個資料流和一個構建器方法,它會在資料流發出新值時重建其子 Widget。

snapshot 參數是一個 AsyncSnapshot,就像 FutureBuilder 一樣。您可以檢查它的 connectionState 屬性,以查看資料流是否尚未傳送任何資料,或者它是否已完全完成。您可以使用 hasError 屬性來查看最新值是否為錯誤。當然,您也可以處理資料值。

最主要的是要確保您的構建器知道如何處理資料流所有可能的狀態。一旦您掌握了這一點,它就可以對資料流的任何操作做出反應。

總結

本文討論了資料流代表什麼、如何從資料流中獲取值、操作這些值的方法,以及 StreamBuilder 如何幫助您在 Flutter 應用程式中使用資料流值。

您可以從 Dart 和 Flutter 文件中了解更多關於資料流的資訊:

或者繼續觀看「Dart 中的非同步程式設計」系列中的下一個影片。它討論了 asyncawait,這是 Dart 提供的兩個關鍵字,可以幫助您保持非同步程式碼的簡潔和易讀性。

非常感謝 Andrew Brogdon,他製作了本文所依據的影片。