Flutter-BLoC-第一講

本篇已同步到 我的博客 ,歡迎常來。react

【譯文】Reactive Programming - Streams - BLoC

本譯文介紹Streams、Bloc 和 Reactive Programming 的概念。理論和實踐範例。對於做者的我的note沒有進行翻譯,請自行翻閱原文地址 原文原碼。和iOS開發中的RAC類似,本文推薦重點在 <如何基於流出的數據構建Widge>!git

難度:中級github

本文紀實

本譯文的原文是在學 BLoC 的 第三方框架 (框架的教程)而看到的推薦連接進入該文章,爲了更好的實現Flutter的BLoC而進行的翻譯學習,翻譯完也到了文章底部居然有推薦中文翻譯 連接, 那本篇就孤芳自賞吧!也順便記錄下本身的第一篇國外技術譯文吧!推薦讀者結合原文 看譯文效果會更佳。 筆者本文學習目的: 解耦編程

什麼是流?

介紹 :爲了便於想象Stream的概念,只需考慮一個帶有兩端的管道,只有一個容許在其中插入一些東西。當你將某物插入管道時,它會在管道內流動並從另外一端流出。後端

在Flutter中api

  • 管道稱爲 Stream
  • 一般(*)使用StreamController來控制Stream
  • 爲了插入東西到Stream中,StreamController公開了"入口"名爲StreamSink,能夠sink屬性進行訪問你
  • StreamController經過stream屬性公開了Stream的出口

注意: (*):我故意使用術語"一般",由於極可能不使用任何StreamController。可是,正如你將在本文中閱讀的那樣,我將只使用StreamControllers。markdown

Stream能夠傳遞什麼?

全部類型值均可以經過流傳遞。從值,事件,對象,集合,映射,錯誤或甚至另外一個流,能夠由stream傳達任何類型的數據。架構

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

當你須要通知Stream傳達某些內容時,你只須要監聽StreamControllerstream屬性。app

定義監聽器時,你會收到StreamSubscription對象。經過StreamSubscription對象,你將收到由Stream發生變化而觸發通知。框架

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

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

StreamSubscription對象也能夠容許如下操做:

  • 中止聽
  • 暫停,
  • 恢復。

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

不,Stream還容許在流出以前處理流入其中的數據。

爲了控制Stream內部數據的處理,咱們使用StreamTransformer,它只是

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

你將直接從該聲明中瞭解到,能夠按順序使用多個StreamTransformer。

StreamTransformer能夠用進行任何類型的處理,例如:

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

Stream流的類型

Stream有兩種類型。

單訂閱Stream

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

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

廣播流

第二種類型的Stream容許任意數量的監聽器。

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

基本的例子

任何類型的數據

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

streams_1.dart

import 'dart:async';

void main() {
  //
  // 初始化「單訂閱」流控制器
  //
  final StreamController ctrl = StreamController();
  
  //
   //初始化一個只打印數據的監聽器
  //一收到它
  //
  final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));

  //
  // 咱們在這裏添加將會流進Stream中的數據
  //
  ctrl.sink.add('my name');
  ctrl.sink.add(1234);
  ctrl.sink.add({'a': 'element A', 'b': 'element B'});
  ctrl.sink.add(123.45);
  
  //
  // 咱們發佈了StreamController
  //
  ctrl.close();
}
複製代碼
StreamTransformer

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

import 'dart:async';

void main() {
  //
  // Initialize a "Broadcast" Stream controller of integers
  //
  final StreamController<int> ctrl = StreamController<int>.broadcast();
  
  //
  // Initialize a single listener which filters out the odd numbers and
  // only prints the even numbers
  //
  final StreamSubscription subscription = ctrl.stream
					      .where((value) => (value % 2 == 0))
					      .listen((value) => print('$value'));

  //
  // We here add the data that will flow inside the stream
  //
  for(int i=1; i<11; i++){
  	ctrl.sink.add(i);
  }
  
  //
  // We release the StreamController
  //
  ctrl.close();
}
複製代碼

RxDart

所述RxDart包是用於執行 Dart 所述的ReactiveX API,它擴展了原始達特流 API符合ReactiveX標準。 因爲它最初並未由Google定義,所以它使用不一樣的詞彙表。下表給出了Dart和RxDart之間的相關性。

Dart RxDart
Stream Observable
StreamController Subject

正如剛纔所說,RxDart 擴展了原始的Dart Streams API並提供了StreamController的 3個主要變體:

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已經發出的全部事件做爲第一個事件發送給任何新的監聽器。

關於資源的重要說明

常常釋放再也不須要的資源是一種很是好的作法。 本聲明適用於:

  • StreamSubscription - 當你再也不須要監聽Stream時,取消訂閱;
  • StreamController - 當你再也不須要StreamController時,關閉它;
  • 這一樣適用於RxDart主題,當你再也不須要BehaviourSubject,PublishSubject ...時,請將其關閉。

如何基於由Stream提供的數據構建Widget?(重點)

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

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

這是如何使用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
    },
)
複製代碼

如下示例模仿默認的 「計數器」 應用程序,但使用Stream而再也不使用任何setState。

import 'dart:async';
import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
  final StreamController<int> _streamController = StreamController<int>();

  @override
  void dispose(){
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
      // 咱們正在監聽流,每次有一個新值流出這個流時,咱們用該值更新Text ;
        child: StreamBuilder<int>(
          stream: _streamController.stream,
          initialData: _counter,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
        //當咱們點擊FloatingActionButton時,增長計數器並經過sink將其發送到Stream;
        //事實上 注入到stream中值會致使監聽它(stream)的StreamBuilder重建並 ‘刷新’計數器;
          _streamController.sink.add(++_counter);
        },
      ),
    );
  }
}
複製代碼
注意點:
  • 24-30行: 咱們再也不須要state的概念,全部東西都經過Stream接受;

第35行:當咱們點擊FloatingActionButton時,咱們遞增計數器並經過接收器將其發送到Stream; 在流中注入值的事實致使偵聽它的StreamBuilder重建並「刷新」計數器;

  • 這是一個很大的改進,由於實際調用setState()方法的,會強制整個 Widget(和任何子小部件)重建。這裏,只有StreamBuilder被重建(固然它的子部件,被streamBuilder包裹的子控件);

  • 咱們仍然在爲頁面使用StatefulWidget的惟一緣由,僅僅是由於咱們須要經過dispose方法第15行釋放StreamController ;

什麼是反應式編程?

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

很明顯,全部這些意味着,經過反應式編程,應用程序:
  • 變得異步
  • 圍繞Streams和listeners的概念進行架構
  • 當某事發生在某處(事件,變量的變化......)時,會向Stream發送通知
  • 若是 "某人" 監聽該流(不管其在應用程序中的任何位置),它將被通知並將採起適當的行動.
組件之間再也不存在緊密耦合。

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

  • 接下來會發生什麼
  • 誰可能使用這些信息(沒有一個,一個或幾個小部件......)
  • 可能使用此信息的地方(無處,同一屏幕,另外一個,幾個...)
  • 當這些信息可能被使用時(幾乎是直接,幾秒鐘以後,永遠不會......)
  • ...... Widget只關心本身的事業,就是這樣!
乍一看,讀到這個,這彷佛會致使應用程序「 沒法控制 」,但正如咱們將看到的,狀況正好相反。它給你:
  • 構建僅負責特定活動的部分應用程序的機會
  • 輕鬆模擬一些組件的行爲,以容許更完整的測試覆蓋
  • 輕鬆重用組件(當前應用程序或其餘應用程序中的其餘位置),
  • 從新設計應用程序,並可以在不進行太多重構的狀況下將組件從一個地方移動到另外一個地方,

咱們將很快看到優點......但在我須要介紹最後一個主題以前:BLoC模式。


BLoC 模式

BLoC模式由Paolo Soares 和 Cong Hui設計,並谷歌在2018的 DartConf 首次提出,能夠在 YouTube 上觀看。

BLoC表示爲業務邏輯組件 (Business Logic Component)

簡而言之, Business Logic須要:

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

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

它究竟意味着什麼?

BLoC模式 是利用咱們剛纔上面所討論的觀念:Streams (流)

image.png

  • Widgets 經過 Sinks 向 BLoC 發送事件(event)
  • BLoC 經過流(stream)通知小部件(widgets)
  • 由BLoC實現的業務邏輯不是他們關注的問題。
從這個聲明中,咱們能夠直接看到一個巨大的好處。

因爲業務邏輯與UI的分離:

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

將 BLoC 模式應用於此計數器應用程序彷佛有點矯枉過正,但讓我先向你展現......

代碼: streams_4.dart

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        title: 'Streams Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: BlocProvider<IncrementBloc>(
          bloc: IncrementBloc(),
          child: CounterPage(),
        ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
        child: StreamBuilder<int>(
          stream: bloc.outCounter,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          bloc.incrementCounter.add(null);
        },
      ),
    );
  }
}

class IncrementBloc implements BlocBase {
  int _counter;

  //
  // Stream來處理計數器
  //
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  //
  // Stream來處理計數器上的操做
  //
  StreamController _actionController = StreamController();
  StreamSink get incrementCounter => _actionController.sink;

  //
  // Constructor
  //
  IncrementBloc(){
    _counter = 0;
    _actionController.stream
                     .listen(_handleLogic);
  }

  void dispose(){
    _actionController.close();
    _counterController.close();
  }

  void _handleLogic(data){
    _counter = _counter + 1;
    _inAdd.add(_counter);
  }
}
複製代碼

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

第一 是責任分離
若是你檢查CounterPage(第21-45行),其中絕對沒有任何業務邏輯。

此頁面如今僅負責:

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

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

若是如今,你須要更改業務邏輯,你只需更新方法_handleLogic(第77-80行)。也許新的業務邏輯將要求作很是複雜的事情...... CounterPage永遠不會知道它,這是很是好的!
複製代碼
第二 可測試性
如今,測試業務邏輯變得更加容易。

無需再經過用戶界面測試業務邏輯。只須要測試IncrementBloc類。
複製代碼
第三 自由組織布局
因爲使用了Streams,你如今能夠獨立於業務邏輯組織布局。

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

你能夠在任何頁面的任何位置顯示計數器,只需聽取.outCounter stream。
複製代碼
第四 減小「build」的次數
不使用setState()而是使用StreamBuilder這一事實大大減小了「 構建 」的次數,只減小了所需的次數。

從性能角度來看,這是一個巨大的進步。
複製代碼
只有一個約束...... BLoC的可訪問性

爲了讓全部這些工做,BLoC須要可訪問。

有幾種方法能夠訪問它:

  • 經過全局單例 這種方式頗有簡單,但不是真的推薦。此外,因爲Dart中沒有類析構函數,所以你永遠沒法正確釋放資源。

  • 做爲局部變量(本地實例) 你能夠實例化BLoC的本地實例。在某些狀況下,此解決方案徹底符合某些需求。在這種狀況下,你應該始終考慮在StatefulWidget中初始化,以便你能夠利用dispose()方法來釋放它。

  • 由父類提供 使其可訪問的最多見方式是經過祖先 Widget,實現爲StatefulWidget。

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

代碼: streams_5

//全部BLoC的通用接口
abstract class BlocBase {
  void dispose();
}

//通用BLoC提供商
class BlocProvider<T extends BlocBase> extends StatefulWidget {
  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }): super(key: key);

  final T bloc;
  final Widget child;

  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context){
    final type = _typeOf<BlocProvider<T>>();
    BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
    return provider.bloc;
  }

  static Type _typeOf<T>() => T;
}

class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
  @override
 /// 便於資源的釋放
  void dispose(){
    widget.bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return widget.child;
  }
}
複製代碼
關於這種通用BlocProvider的一些解釋

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

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

home: BlocProvider<IncrementBloc>(
          bloc: IncrementBloc(),
          child: CounterPage(),
        ),
複製代碼

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

從那一刻開始,從BlocProvider開始的子樹的任何小部件部分都將可以經過如下代碼訪問IncrementBloc:

IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
複製代碼
可使用多個BLoC嗎?

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

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

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

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

代碼 streams_6.dart

void main() => runApp(
  BlocProvider<ApplicationBloc>(
    bloc: ApplicationBloc(),
    child: MyApp(),
  )
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    return MaterialApp(
      title: 'Streams Demo',
      home: BlocProvider<IncrementBloc>(
        bloc: IncrementBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    final IncrementBloc counterBloc = BlocProvider.of<IncrementBloc>(context);
    final ApplicationBloc appBloc = BlocProvider.of<ApplicationBloc>(context);
    
    ...
  }
}
複製代碼
爲何不使用InheritedWidget?

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

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

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

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

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

提醒InheritedWidget

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

請注意,爲了徹底正確,我剛纔解釋的與InheritedWidget相關的問題只發生在咱們將InheritedWidget與StatefulWidget結合使用時。當你只使用沒有State的InheritedWidget時,問題就不會發生。可是......我將在下一篇文章 中回到這句話。

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

關於BLoC的我的建議

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

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

首先,BLoC模式被設想爲跨平臺共享相同的代碼(AngularDart,......),而且從這個角度來看,該陳述徹底有意義。

可是,若是你只打算開發一個Flutter應用程序,這是基於個人謙遜經驗,有點矯枉過正。

若是咱們堅持聲明,沒有可能的getter或setter,只有sink和stream。缺點是「全部這些都是異步的」。

讓咱們用2個樣原本說明缺點:

你須要從BLoC中檢索一些數據,以便將這些數據用做應該當即顯示這些參數的頁面的輸入(例如,想一個參數頁面),若是咱們不得不依賴Streams,這使得頁面的構建異步(這很複雜)。經過Streams使其工做的示例代碼可能以下所示......很醜陋不是嗎。

代碼 streams_7.dart 以下:

class FiltersPage extends StatefulWidget {
  @override
  FiltersPageState createState() => FiltersPageState();
}

class FiltersPageState extends State<FiltersPage> {
  MovieCatalogBloc _movieBloc;
  double _minReleaseDate;
  double _maxReleaseDate;
  MovieGenre _movieGenre;
  bool _isInit = false;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    // 做爲initState()級別還沒有提供的上下文,若是還沒有初始化,咱們將得到過濾器參數列表
    if (_isInit == false){
      _movieBloc = BlocProvider.of<MovieCatalogBloc>(context);
      _getFilterParameters();
    }
  }

  @override
  Widget build(BuildContext context) {
    return _isInit == false
      ? Container()
      : Scaffold(
    ...
    );
  }

  ///
  /// 很是棘手.
  /// 
  /// 因爲咱們但願100%符合BLoC標準,咱們須要使用Streams從BLoCs中檢索全部內容......
  /// 
  /// 這很難看,但被視爲一個研究案例。
  ///
  void _getFilterParameters() {
    StreamSubscription subscriptionFilters;

    subscriptionFilters = _movieBloc.outFilters.listen((MovieFilters filters) {
        _minReleaseDate = filters.minReleaseDate.toDouble();
        _maxReleaseDate = filters.maxReleaseDate.toDouble();

        // 只需確保訂閱已發佈
        subscriptionFilters.cancel();
        
        // 如今咱們有了全部參數,咱們能夠構建實際的頁面
        if (mounted){
          setState((){
            _isInit = true;
          });
        }
      });
    });
  }
}
複製代碼

在BLoC級別,您還須要轉換某些數據的「假」注入,以觸發提供您但願經過流接收的數據。使這項工做的示例代碼能夠是:

代碼streams_8.dart

class ApplicationBloc implements BlocBase {
  ///
  /// 同步流來處理提供的電影類型
  ///
  StreamController<List<MovieGenre>> _syncController = StreamController<List<MovieGenre>>.broadcast();
  Stream<List<MovieGenre>> get outMovieGenres => _syncController.stream;

  ///
  /// 流處理假命令以經過Stream觸發提供MovieGenres列表
  ///
  StreamController<List<MovieGenre>> _cmdController = StreamController<List<MovieGenre>>.broadcast();
  StreamSink get getMovieGenres => _cmdController.sink;

  ApplicationBloc() {
    //
    // 若是咱們經過此接收器接收任何數據,咱們只需將MovieGenre列表提供給輸出流
    //
    _cmdController.stream.listen((_){
      _syncController.sink.add(UnmodifiableListView<MovieGenre>(_genresList.genres));
    });
  }

  void dispose(){
    _syncController.close();
    _cmdController.close();
  }

  MovieGenresList _genresList;
}

// Example of external call
BlocProvider.of<ApplicationBloc>(context).getMovieGenres.add(null);
複製代碼

我不知道你的意見,但就我的而言,若是我沒有任何與代碼移植/共享相關的限制,我發現這過重了,我寧願在須要時使用常規的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調用此頁面;
  1. 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(「最喜歡的」圖標) 強制重建_buildMoieCard(「最喜歡的」圖標) 用於構建每一個MovieDetailsWidget

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.builder和ListView.builder都將itemCount做爲輸入,若是提供了item數量,則表示要根據itemCount的數量來顯示列表。itemBuilder的index從0到itemCount - 1不等。

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

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

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

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

//通知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發佈。 其餘一些有趣的文章值得一讀:

Fundamentals of Dart Streams [Thomas Burkhart]

rx_command package [Thomas Burkhart]

Build reactive mobile apps in Flutter - companion article [Filip Hracek]

Flutter with Streams and RxDart [Brian Egan]

總結

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

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

這篇文章也能夠在 Medium -Flutter Community 找到。

本文源碼

如需轉載本譯文,請註明出處.

相關文章
相關標籤/搜索