[譯]Flutter響應式編程:Streams和BLoC

想看原文請出門右轉原文傳送門
版本全部,轉載請註明出處
本文主要介紹Streams,Bloc和Reactive Programming(響應式編程)的概念。 理論和實踐範例。html

難度:中級react

介紹

我花了很長時間才找到介紹Reactive Programming,BLoC和Streams概念的方法。
因爲這能夠對構建應用程序的方式作出重大改變,我想要一個實際示例來講明:git

  • 極可能不使用它們,但有時可能更難以編碼和性能更低,
  • 使用它們的好處同時也是
  • 使用它們的影響,正面的和(或)負面的。

用我作的僞應用程序做爲一個例子,簡而言之,它容許用戶從在線目錄中查看電影列表,按類型和發佈日期過濾它們,標記/取消標記爲收藏夾。 固然,一切都是互動的,用戶能夠在不一樣的頁面中或在同一個頁面內發生各類動做,而且能夠實時觀察到結果。
下面的動畫展現了該程序:
image.pnggithub

當您進入此頁面以獲取有關Reactive Programming,BLoC和Streams的信息時,我將首先介紹它們。 此後,我將向您展現如何在實踐中實施和使用它們。編程

什麼是Stream?

介紹

爲了便於想象Stream的概念,咱們能夠簡單把Stream想象爲一個有兩個端口的管道,只有其中的一個容許插入一些東西。 當您將某物插入管道時,它會在管道內流動並從另外一端流出。
In Flutter,後端

  • the pipe is called a Stream
  • to control the Stream, we usually<upper style="box-sizing: border-box;">(*)</upper> use a StreamController
  • to insert something into the Stream, the StreamController exposes the 「entrance」, called a StreamSink, accessible via the sink property
  • the way out of the Stream, is exposed by the StreamController via the streamproperty

在Flutter中,api

  • 管道稱爲Stream
  • 爲了控制Stream,咱們一般(*)使用StreamController
  • 爲了在Stream中插入一些東西,StreamController公開了一個名爲StreamSink的「入口」,能夠經過sink屬性訪問
  • Stream 流出方式是由StreamController經過stream屬性暴露的。

(*):我故意使用術語「一般」,由於極可能不使用任何StreamController。 可是,正如您將在本文中看到的那樣,我將只使用StreamControllers架構

Stream能夠傳達什麼?

全部類型以及任何類型。 從值,事件,對象,集合,映射,錯誤或甚至另外一個流,任何類型的數據均可以由Stream傳遞 。app

 我怎麼知道Stream傳達的東西?

當您須要通知`Stream`傳達某些內容時,您只須要監聽`StreamController`的`stream`屬性。less

定義監聽時,你會獲得StreamSubscription對象。 經過StreamSubscription對象,你將會接受到通知因爲Stream發生變化而帶來的的通知。

只要至少有一個活動偵聽器,Stream就會開始生成事件,以便每次都通知活動的StreamSubscription對象:

  • 一些數據來自流,
  • 當一些錯誤發送到流時,
  • 當流關閉時。

StreamSubscription也容許如下操做:

  • 中止監聽
  • 暫時
  • 恢復

Stream只是一個簡單的管道嗎?

不,Stream還容許在流出以前處理流入其中的數據。
爲了控制Stream內部數據的處理,咱們使用StreamTransformer,它只是:

  • 一個「捕獲」Stream內部流動數據的函數
  • 對數據作一些處理
  • 這種轉變的結果也是一個Stream

到此你應該很容易意識到你能夠按順序使用多個[StreamTransformer]()。

StreamTransformer可用於進行任何類型的處理,例如:

  • 過濾:根據任何類型的條件過濾數據,
  • 從新組合:從新組合數據,
  • 修改:對數據應用任何類型的修改,
  • 將數據注入其餘流,
  • 緩衝,
  • 處理:根據數據進行任何類型的操做/操做,

...

Stream的類型

Stream有兩種類型。

單訂閱Stream

這種類型的Stream只容許在該Stream的整個生命週期內使用單個監聽器。

即便在第一個訂閱被取消後,也沒法在此類流上收聽兩次。

廣播Stream

這是第二種類型Stream,這種Stream容許任意個數的監聽器。

能夠隨時向廣播流添加監聽器。 新的監聽器將在它開始收聽 Stream時收到事件。

基本例子

任何類型的數據

第一個示例顯示了「單訂閱」Stream,它只是打印輸入的數據。 你可能會看到可有可無的數據類型。

StreamTransformer

第二個示例顯示「廣播」Stream,它傳達整數值並僅打印偶數。 爲此,咱們應用StreamTransformer來過濾(第14行)值,只讓偶數通過。

RxDart

現在,若是我不說起RxDart,那麼Streams的介紹將再也不完整。

RxDart是ReactiveX API的Dart實現,它擴展了原始的Dart Streams API以符合ReactiveX標準。

因爲它最初並未由Google定義,所以它使用不一樣的詞彙表。 下表給出了DartRxDart之間的相關性:
| Dart | RxDart |
| :-------- | --------:|
| Stream | Observable |
| StreamController | Subject |
RxDart 正如我剛剛所說的,繼承了原生的[Dart Streams API]() 而且提供了3種主要的StreamController變種:

PublishSubject

PublishSubject是一個普通的廣播StreamController,但有一種狀況是例外的:當stream返回一個Observable而不是一個[Stream]()時。
image.png
如你所見,PublishSubject僅向監聽器發送在訂閱以後添加到Stream的事件。

BehaviorSubject

BehaviorSubject也是一個廣播StreamController,它返回一個[Observable]()而不是一個[Stream]()。
image.png
PublishSubject的主要區別在於BehaviorSubject還將最後發送的事件發送給剛剛訂閱的監聽器。

ReplaySubject

ReplaySubject也是一個廣播StreamController,它返回一個[Observable]()而不是一個[Stream]()。
image.png
默認狀況下,ReplaySubjectStream已經發出的全部事件做爲第一個事件發送到任何新的監聽器。

關於Resources的重要說明

始終釋放再也不須要的Resources是一種很是好的作法。
適用於:
  • StreamSubscription - 當您再也不須要收聽Stream時,取消訂閱;
  • StreamController - 當你再也不須要StreamController時,關閉它;
  • 這一樣適用於RxDart Subjects,當你再也不須要BehaviourSubjectPublishSubject ...時,請將其關閉。

如何基於由Stream提供的數據構建Widget?

Flutter提供了一個很是方便的StatefulWidget,稱爲StreamBuilder

StreamBuilder監聽Stream,每當某些數據輸出Stream時,它會自動重建,調用其builder回調。

下面的代碼演示瞭如何使用StreamBuilder:

StreamBuilder<T>(
    key: ...optional, the unique ID of this Widget...
    stream: ...the stream to listen to...
    initialData: ...any initial data, in case the stream would initially be empty...
    builder: (BuildContext context, AsyncSnapshot<T> snapshot){
        if (snapshot.hasData){
            return ...the Widget to be built based on snapshot.data
        }
        return ...the Widget to be built if no data is available
    },
)

如下示例模仿默認的「 counter」應用程序,但咱們將使用Stream而再也不使用任何setState。

注:counter是flutter的默認生成的demo。

解釋和說明:

  • 第24-30行:咱們正在監聽stream,每當stream輸出一個新的值,咱們將用該值更新Text;
  • 第35行:當咱們點擊FloatingActionButton時,咱們遞增計數器並經過接收器將其發送到Stream; 在流中注入值的事實致使偵聽它的StreamBuilder重建並「刷新」計數器;
  • 咱們再也不須要State的概念,全部內容都經過Stream接收;
  • 這是一個很大的改進,由於調用setState()方法會強制整個Widget(和任何子窗口小部件)重建。 在這裏,只重建StreamBuilder(固然還有子窗口小部件);
  • 咱們仍然在爲頁面使用StatefulWidget的惟一緣由,僅僅是由於咱們須要經過dispose方法釋放StreamController,第15行;

什麼是響應式編程?

響應式編程是使用異步數據流進行編程。換句話說,從事件(例如,點擊),變量的變化,消息,......到構建請求,可能改變或發生的全部事物的全部內容將被傳送,由數據流觸發。

很明顯,全部這些意味着,經過響應應式編程,應用程序將會:

  • 變得異步,
  • 圍繞Streams和listeners的概念進行架構,
  • 當某些事情發生在某個地方(事件,變量的變化......)時,會向Stream發送通知,
  • 若是「某人」收聽該Stream,它將被通知並將採起適當的行動,不管其在應用程序中的位置如何。
組件之間再也不存在緊密耦合。

簡而言之,當WidgetStream發送內容時,該Widget再也不須要知道:

  • 接下來會發生什麼,
  • 誰可能使用這些信息(沒有一個,一個或幾個Widget...)
  • 可能使用此信息的地方(無處,同一頁面,另外一個頁面,或者幾個頁面...),
  • 當這些信息可能被使用時(幾乎是直接,幾秒鐘以後,永遠不會......)。
...... Widget只關心本身的業務,就是這樣!

乍一看,讀到這個,這彷佛可能致使應用程序的「沒法控制」,但正如咱們將看到的,狀況偏偏相反。 它給你:

  • 構建僅負責特定活動的部分應用程序的機會,
  • 輕鬆模擬一些組件的行爲,以容許更完整的測試覆蓋,
  • 輕鬆重用組件(當前應用程序或其餘應用程序中的其餘位置),
  • 從新設計應用程序,並可以在不進行太多重構的狀況下將組件從一個地方移動到另外一個地方,
  • ...

咱們將很快看到使用響應式編程的好處......但在此以前我還須要介紹一下最後一個話題:BLoC模式。

BLoC模式

BLoC模式由來自Google的Paolo Soares和Cong Hui設計,並在2018年DartConf期間(2018年1月23日至24日)首次展現。 在YouTube上觀看此視頻。

BLoC表明業務邏輯組件(Business Logic Component)。

簡而言之,業務邏輯(Business Logic )須要:

  • 轉移到一個或幾個BLoC,
  • 儘量從表現層中刪除。 換句話說,UI組件應該只關心UI事物而不關心業務,
  • 依賴Streams獨家使用輸入(Sink)和輸出(流),
  • 保持平臺獨立,
  • 保持環境獨立。

事實上,BLoC模式最初被設想爲容許獨立於平臺重用相同的代碼:Web應用程序,移動應用程序,後端。

它到底意味着什麼?

BLoC模式利用了咱們剛纔討論過的概念:Streams。
image.png

  • Widgets經過Sinks向BLoC發送事件,
  • BLoC經過Stream通知Widgets,
  • 由BLoC實現的業務邏輯不是他們關注的問題。

從上面來看,咱們能夠直接看到使用BLoC的一個巨大的好處。

感謝業務邏輯與UI的分離:

  • 咱們能夠隨時更改業務邏輯,對應用程序的影響最小,
  • 咱們可能會更改UI而不會對業務邏輯產生任何影響,
  • 如今,測試業務邏輯變得更加容易。

如何將此BLoC模式應用於Counter應用?

將BLoC模式應用於Counter 應用可能看起來有點矯枉過正,但請容許我先向你展現......

我已經聽到你說「哇......爲何這一切? 這一切都是必要的嗎?「

首先,是責任分離

若是你檢查CounterPage(第21-45行),你會發現其中絕對沒有任何業務邏輯。

此頁面如今僅負責:

  • 顯示計數器,如今只在必要時刷新(即便頁面沒必要知道)
  • 提供按鈕,當按鈕按下時,將會在counter面板上請求一個動做

此外,整個業務邏輯集中在一個單獨的類「IncrementBloc」中。

如今若是你須要更改業務邏輯,您只需更新方法_handleLogic(第77-80行)。 也許新的業務邏輯會要求作很是複雜的事情...... CounterPage永遠不會知道它,這很是好!

其次,可測試性

如今,測試業務邏輯變得更加容易。

無需再經過UI測試業務邏輯。 只須要測試IncrementBloc

第三,自由組織布局

因爲使用了Streams,你如今能夠獨立於業務邏輯組織布局。

能夠從應用程序中的任何位置啓動任何操做:只需調用.incrementCounter sink便可。

您能夠在任何頁面的任何位置顯示counter,只需聽取.outCounter stream。

第四,減小「build」的數量

不使用setState()而是使用StreamBuilder大大減小了「build」的數量。

從性能角度來看,這是一個巨大的進步。

只有一個限制...BLoC的可訪問性

爲了使全部這些工做,BLoC須要能夠被訪問到。

有幾種方法能夠訪問它:

  • 經過全局單例

    這種方式能夠實現,但不是真的推薦。 此外,因爲Dart中沒有類析構函數,所以你永遠沒法正確釋放資源。

  • 做爲局部變量

    你能夠實例化BLoC的局部實例。 在某些狀況下,此解決方案徹底符合某些需求。 在這種狀況下,你應該始終考慮在StatefulWidget中初始化,以便您
    能夠利用dispose()方法來釋放相關資源。

  • 由父級提供

    使其可訪問的最多見方式是經過父級Widget訪問,經過StatefulWidget實現。

如下代碼顯示了通用BlocProvider的示例。

關於這種通用BlocProvider的一些解釋

首先,如何將其做爲provider使用?

若是你查看示例代碼「streams_4.dart」,你將看到如下代碼行(第12-15行)

home: BlocProvider<IncrementBloc>(
          bloc: IncrementBloc(),
          child: CounterPage(),
        ),

經過這些代碼,咱們只需實例化一個新的BlocProvider,它將處理一個IncrementBloc,並將CounterPage做爲子項呈現。

從那一刻開始,從BlocProvider開始的子樹的任何Widget都將可以經過以代碼訪問IncrementBloc:

IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);

可使用多個BLoC嗎?

固然,這是很是可取的。建議以下:

  • (若是有任何業務邏輯)每一個頁面的頂部有一個BLoC,
  • 爲何不是ApplicationBloc來處理應用程序狀態?
  • 每一個「足夠複雜的組件」都有相應的BLoC。

如下示例代碼在整個應用程序的頂部顯示ApplicationBloc,而後在CounterPage頂部顯示IncrementBloc。

該示例還顯示瞭如何檢索兩個bloc。

爲何不使用InheritedWidget?

在與BLoC相關的大多數文章中,你會看到經過InheritedWidget實現Provider。

固然,沒有什麼能阻止這種類型的實現。 然而,

  • 一個InheritedWidget沒有提供任何dispose方法,請記住,在再也不須要資源時老是釋放資源是一種很好的作法。
  • 固然,沒有什麼能阻止你將InheritedWidget包裝在另外一個StatefulWidget中,可是,使用InheritedWidget增長了什麼呢?
  • 最後,若是不受控制,使用InheritedWidget常常會致使反作用(請參閱下面的InheritedWidget上的Reminder)。

這三點解釋了我爲何選擇經過StatefulWidget實現BlocProvider,這樣作可讓我在Widget dispose時釋放相關資源。

Flutter沒法實例化泛型類型
不幸的是,Flutter沒法實例化泛型類型,咱們必須將BLoC的實例傳遞給BlocProvider。 爲了在每一個BLoC中強制執行dispose()方法,全部BLoC都必
須實現BlocBase接口。

InheritedWidget的一些提醒

在使用InheritedWidget並經過context.inheritFromWidgetOfExactType(...)獲取指定類型最近的Widget時,每當InheritedWidget的父級或者子佈局發生變化時,這個方法會自動將當前「context」(= BuildContext)註冊到要重建的widget當中。

連接到BuildContext的Widget(Stateful或Stateless)的類型可有可無。

關於BLoC的我的建議

與BLoC相關的第三條規則是:「依賴於Streams對輸入(Sink)和輸出(stream)的獨佔使用」。

個人我的經歷稍微關係到這個說法......讓我解釋一下。

起初,BLoC模式被設想爲跨平臺共享相同的代碼(AngularDart,...),而且從這個角度來看,該語句很是有意義。

可是,若是您只打算開發一個Flutter應用程序,那麼根據個人謙遜經驗,這有點矯枉過正。

若是咱們堅持這種說法,那麼就沒有getter或settr,只有sinkstream。缺點是「全部這些都是異步的」。

咱們來看兩個樣原本說明缺點:

  • 你須要從BLoC中檢索一些數據,以便使用這些數據做爲應該當即顯示這些參數的頁面的輸入(例如,想一個參數頁面),若是咱們不得不依賴Streams,這會使構建異步頁面(很複雜)。經過Streams使其工做的示例代碼可能以下所示......醜陋不是它。
  • 在BLoC級別,您還須要轉換某些數據的「假」注入,以觸發提供您但願經過流接收的數據。使這項工做的示例代碼能夠是:

我不知道您的意見,但就我的而言,若是我沒有任何與代碼移植/共享相關的限制,我發現這太笨重了,我寧願在須要時使用常規的getter / setter並使用Streams / Sinks來保持分離責任並在須要的地方廣播信息,這很棒。

如今是時候在實踐中看到這一切......

正如本文開頭所提到的,我構建了一個僞應用程序來展現如何使用全部這些概念。 完整的源代碼能夠在Github上找到。

請放縱,由於這段代碼遠非完美,可能會作的更好和(或)有更好的架構,但惟一的目標只是告訴你這一切是如何工做的。

因爲源代碼太多不少,我只會解釋主要的幾條。

電影目錄的來源

我使用免費的TMDB API來獲取全部電影的列表,以及海報,評級和描述。

爲了可以運行此示例應用程序,您須要註冊並獲取API密鑰(徹底免費),而後將您的API密鑰放在文件「/api/tmdb_api.dart」第15行。

應用程序的體系結構

該應用程序使用到了:

  • 3個主要的BLoC:

    1. ApplicationBloc(在全部內容之上),負責提供全部電影類型的列表;
2.*FavoriteBloc*(就在下面),負責處理「收藏夾」的概念;
   3.*MovieCatalogBloc*(在2個主要頁面之上),負責根據過濾器提供電影列表;
  • 6個頁面:
    1.HomePage:登錄頁面,容許導航到3個子頁面;
    2.ListPage:將電影列爲GridView的頁面,容許過濾,收藏夾選擇,訪問收藏夾以及在後續頁面中顯示電影詳細信息;
    3.ListOnePage:相似於ListPage,但電影列表顯示爲水平列表,下面是詳細信息;

    1. FavoritesPage:列出收藏夾的頁面,容許取消選擇任何收藏夾;
5.* Filters*:容許定義過濾器的EndDrawer:流派和最小/最大發布日期。從ListPage或ListOnePage調用此頁面;
    6.* Details*詳細信息:頁面僅由ListPage調用以顯示電影的詳細信息,但也容許選擇/取消選擇電影做爲收藏;
  • 1個子BLoC:
    1.FavoriteMovieBloc,連接到MovieCardWidget或MovieDetailsWidget,以處理做爲收藏的電影的選擇/取消選擇
  • 5個主要Widget:
    1.FavoriteButton:負責顯示收藏夾的數量,實時,並在按下時重定向到FavoritesPage;
    2.FavoriteWidget:負責顯示一個喜歡的電影的細節並容許其取消選擇;
    3.FiltersSummary:負責顯示當前定義的過濾器;
    4.MovieCardWidget:負責將一部電影顯示爲卡片,電影海報,評級和名稱,以及一個圖標,表示該特定電影的選擇是最喜歡的;
    5.MovieDetailsWidget:負責顯示與特定電影相關的詳細信息,並容許其選擇/取消選擇做爲收藏。

不一樣BLoCs / Streams的編排

下圖顯示瞭如何使用主要3個BLoC:

  • 在BLoC的左側,哪些組件調用Sink
  • 在右側,哪些組件監聽流

例如,當MovieDetailsWidget調用inAddFavorite Sink時,會觸發2個stream:

  • outTotalFavorites流強制重建FavoriteButton,和
  • outFavorites流

    • 強制重建MovieDetailsWidget(「最喜歡的」圖標)
    • 強制重建_buildMovieCard(「最喜歡的」圖標)
    • 用於構建每一個MovieDetailsWidget

image.png
image.png
image.png

觀察

大多數Widget和Page都是StatelessWidgets,這意味着:

  • 強制重建的setState()幾乎從未使用過。 例外狀況是:

    • 在ListOnePage中,當用戶點擊MovieCard時,刷新MovieDetailsWidget。 這也多是由一個stream驅動的......
    • 在FiltersPage中容許用戶在接受篩選條件以前經過Sink更改過篩選條件。
  • 應用程序不使用任何InheritedWidget
  • 該應用程序幾乎是100%BLoCs / Streams驅動,這意味着大多數小部件彼此獨立,而且它們在應用程序中的位置

一個實際的例子是FavoriteButton,它顯示徽章中所選收藏夾的數量。 該應用程序共有3個FavoriteButton實例,每一個實例顯示在3個不一樣的頁面中。

顯示電影列表(顯示無限列表的技巧說明)

要顯示符合過濾條件的電影列表,咱們使用GridView.builder(ListPage)或ListView.builder(ListOnePage)做爲無限滾動列表。

電影是經過TMDB API獲取的,每次拉取20個。

提醒一下,GridView.builderListView.builder都將itemCount做爲輸入,若是提供了item數量,則表示要根據itemCount的數量來顯示列表。itemBuilderindex從0到itemCount - 1不等。

正如您將在代碼中看到的那樣,我隨意爲GridView.builder添加了30多個。 理由是,在這個例子中,咱們正在操縱假定的無限數量的項目(這不是徹底正確可是又有誰關心這個例子)。 這將強制GridView.builder請求顯示「最多30個」項目。

此外,GridView.builderListView.builder只在認爲必須在視口中呈現某個項目(索引)時才調用itemBuilder。

MovieCatalogBloc.outMoviesList返回一個List <MovieCard>,它被迭代以構建每一個Movie Card。 第一次,這個List <MovieCard>是空的,可是因爲itemCount:... + 30,咱們欺騙系統,它將要求經過_buildMovieCard(...)呈現30個不存在的項目。

正如您將在代碼中看到的,此例程對Sink進行了一次奇怪的調用:

// Notify the MovieCatalogBloc that we are rendering the MovieCard[index]
//通知MovieCatalogBloc咱們正在渲染MovieCard[index]
    movieBloc.inMovieIndex.add(index);

這個調用告訴MovieCatalogBloc咱們要渲染MovieCard [index]。

而後_buildMovieCard(...)繼續驗證與MovieCard [index]相關的數據是否存在。 若是是,則渲染後者,不然顯示CircularProgressIndicator。

StreamCatalogBloc.inMovieIndex.add(index)的調用由StreamSubscription監聽,StreamSubscription將索引轉換爲某個pageIndex數字(一頁最多可計20部電影)。 若是還沒有從TMDB API獲取相應頁面,則會調用API。 獲取頁面後,全部已獲取電影的新列表將發送到_moviesController。 當GridView.builder監聽該Stream(= movieBloc.outMoviesList)時,後者請求重建相應的MovieCard。 因爲咱們如今擁有數據,咱們能夠渲染它了。

名單和其餘連接

介紹PublishSubject,BehaviorSubject和ReplaySubject的圖片由ReactiveX發佈。

其餘一些有趣的文章值得一讀:

結論

很長的文章,但還有更多的話要說,由於對我而言,這是展開Flutter應用程序的方法。 它提供了很大的靈活性。

很快就會繼續關注新文章。 快樂寫代碼。

文中所涉及到的源碼

版本全部,轉載請註明出處

相關文章
相關標籤/搜索