Flutter-BLoC-第一講

Silence_Zhou發表於2019-04-02

本篇已同步到 個人部落格 ,歡迎常來。

【譯文】Reactive Programming - Streams - BLoC

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

難度:中級

本文紀實

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

什麼是流?

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

在Flutter中

  • 管道稱為 Stream
  • 通常(*)使用StreamController來控制Stream
  • 為了插入東西到Stream中,StreamController公開了"入口"名為StreamSink,可以sink屬性進行訪問你
  • StreamController通過stream屬性公開了Stream的出口

注意: (*):我故意使用術語"通常",因為很可能不使用任何StreamController。但是,正如你將在本文中閱讀的那樣,我將只使用StreamControllers。

Stream可以傳遞什麼?

所有型別值都可以通過流傳遞。從值,事件,物件,集合,對映,錯誤或甚至另一個流,可以由stream傳達任何型別的資料。

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

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

定義監聽器時,你會收到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 找到。

本文原始碼

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

相關文章