Flutter - BLoC 第一講

本篇已同步到 個(gè)人博客 ,歡迎常來(lái)。

【譯文】Reactive Programming - Streams - BLoC

注:此處的"toc"應(yīng)顯示為目錄,但是簡(jiǎn)書(shū)不支持,顯示不出來(lái)。

[toc]

本譯文介紹Streams、Bloc 和 Reactive Programming 的概念。理論和實(shí)踐范例。對(duì)于作者的個(gè)人note沒(méi)有進(jìn)行翻譯,請(qǐng)自行翻閱原文地址 原文原碼。和iOS開(kāi)發(fā)中的RAC相似,本文推薦重點(diǎn)在 <如何基于流出的數(shù)據(jù)構(gòu)建Widge>!

難度:中級(jí)

本文紀(jì)實(shí)

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

什么是流?

介紹 :為了便于想象Stream的概念,只需考慮一個(gè)帶有兩端的管道,只有一個(gè)允許在其中插入一些東西。當(dāng)你將某物插入管道時(shí),它會(huì)在管道內(nèi)流動(dòng)并從另一端流出。

在Flutter中

  • 管道稱(chēng)為 Stream
  • 通常(*)使用StreamController來(lái)控制Stream
  • 為了插入東西到Stream中,StreamController公開(kāi)了"入口"名為StreamSink,可以sink屬性進(jìn)行訪(fǎng)問(wèn)你
  • StreamController通過(guò)stream屬性公開(kāi)了Stream的出口

注意: (*):我故意使用術(shù)語(yǔ)"通常",因?yàn)楹芸赡懿皇褂萌魏蜸treamController。但是,正如你將在本文中閱讀的那樣,我將只使用StreamControllers。

Stream可以傳遞什么?

所有類(lèi)型值都可以通過(guò)流傳遞。從值,事件,對(duì)象,集合,映射,錯(cuò)誤或甚至另一個(gè)流,可以由stream傳達(dá)任何類(lèi)型的數(shù)據(jù)。

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

當(dāng)你需要通知Stream傳達(dá)某些內(nèi)容時(shí),你只需要監(jiān)聽(tīng)StreamControllerstream屬性。

定義監(jiān)聽(tīng)器時(shí),你會(huì)收到StreamSubscription對(duì)象。通過(guò)StreamSubscription對(duì)象,你將收到由Stream發(fā)生變化而觸發(fā)通知。

只要有至少一個(gè)活動(dòng) 監(jiān)聽(tīng)器,Stream就會(huì)開(kāi)始生成事件,以便每次都通知活動(dòng)的 StreamSubscription對(duì)象:

  • 一些數(shù)據(jù)來(lái)自流,
  • 當(dāng)一些錯(cuò)誤發(fā)送到流時(shí),
  • 當(dāng)流關(guān)閉時(shí)。

StreamSubscription對(duì)象也可以允許以下操作:

  • 停止聽(tīng)
  • 暫停,
  • 恢復(fù)。

Stream只是一個(gè)簡(jiǎn)單的管道嗎?

不,Stream還允許在流出之前處理流入其中的數(shù)據(jù)。

為了控制Stream內(nèi)部數(shù)據(jù)的處理,我們使用StreamTransformer,它只是

  • 一個(gè)“捕獲” Stream內(nèi)部流動(dòng)數(shù)據(jù)的函數(shù)
  • 對(duì)數(shù)據(jù)做一些處理
  • 這種轉(zhuǎn)變的結(jié)果也是一個(gè)Stream

你將直接從該聲明中了解到,可以按順序使用多個(gè)StreamTransformer。

StreamTransformer可以用進(jìn)行任何類(lèi)型的處理,例如:

  • 過(guò)濾(filtering):根據(jù)任何類(lèi)型的條件過(guò)濾數(shù)據(jù),
  • 重新組合(regrouping):重新組合數(shù)據(jù),
  • 修改(modification):對(duì)數(shù)據(jù)應(yīng)用任何類(lèi)型的修改,
  • 將數(shù)據(jù)注入其他流,
  • 緩沖,
  • 處理(processing):根據(jù)數(shù)據(jù)進(jìn)行任何類(lèi)型的操作/操作,
  • ...

Stream流的類(lèi)型

Stream有兩種類(lèi)型。

單訂閱Stream

這種類(lèi)型的Stream只允許在該Stream的整個(gè)生命周期內(nèi)使用單個(gè)監(jiān)聽(tīng)器。

即在第一個(gè)訂閱被取消后,也無(wú)法在此類(lèi)流上收聽(tīng)兩次。

廣播流

第二種類(lèi)型的Stream允許任意數(shù)量的監(jiān)聽(tīng)器。

可以隨時(shí)向廣播流添加監(jiān)聽(tīng)器。新的監(jiān)聽(tīng)器將在它開(kāi)始收聽(tīng)Stream時(shí)收到事件。

基本的例子

任何類(lèi)型的數(shù)據(jù)

第一個(gè)示例顯示了“單訂閱” 流,它只是打印輸入的數(shù)據(jù)。你可能會(huì)看到無(wú)關(guān)緊要的數(shù)據(jù)類(lèi)型。

streams_1.dart

import 'dart:async';

void main() {
  //
  // 初始化“單訂閱”流控制器
  //
  final StreamController ctrl = StreamController();
  
  //
   //初始化一個(gè)只打印數(shù)據(jù)的監(jiān)聽(tīng)器
  //一收到它
  //
  final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));

  //
  // 我們?cè)谶@里添加將會(huì)流進(jìn)Stream中的數(shù)據(jù)
  //
  ctrl.sink.add('my name');
  ctrl.sink.add(1234);
  ctrl.sink.add({'a': 'element A', 'b': 'element B'});
  ctrl.sink.add(123.45);
  
  //
  // 我們發(fā)布了StreamController
  //
  ctrl.close();
}
StreamTransformer

第二個(gè)示例顯示“ 廣播 ” 流,它傳達(dá)整數(shù)值并僅打印偶數(shù)。為此,我們應(yīng)用StreamTransformer來(lái)過(guò)濾(第14行)值,只讓偶數(shù)經(jīng)過(guò)。

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包是用于執(zhí)行 Dart 所述的ReactiveX API,它擴(kuò)展了原始 Dart Stream API符合ReactiveX標(biāo)準(zhǔn)。
由于它最初并未由Google定義,因此它使用不同的詞匯表。下表給出了Dart和RxDart之間的相關(guān)性。

Dart RxDart
Stream Observable
StreamController Subject

正如剛才所說(shuō),RxDart 擴(kuò)展了原始的Dart Streams API并提供了StreamController的 3個(gè)主要變體:

PublishSubject

PublishSubject是普通的廣播 StreamController, 有一個(gè)例外:Stream返回一個(gè)Observable,而不是Stream。


image.png

如你所見(jiàn),PublishSubject僅向監(jiān)聽(tīng)器發(fā)送在訂閱之后添加到Stream的事件。

BehaviorSubject

該BehaviorSubject也是廣播 StreamController,它返回一個(gè)Observable,而不是Stream。

image.png

與PublishSubject的主要區(qū)別在于BehaviorSubject還將最后發(fā)送的事件發(fā)送給剛剛訂閱的監(jiān)聽(tīng)器。

ReplaySubject

ReplaySubject 也是一個(gè)廣播StreamController,它返回一個(gè)Observable,而不是Stream。

image.png

默認(rèn)情況下,ReplaySubjectStream已經(jīng)發(fā)出的所有事件作為第一個(gè)事件發(fā)送給任何新的監(jiān)聽(tīng)器。

關(guān)于資源的重要說(shuō)明

經(jīng)常釋放不再需要的資源是一種非常好的做法。
本聲明適用于:

  • StreamSubscription - 當(dāng)你不再需要監(jiān)聽(tīng)Stream時(shí),取消訂閱;
  • StreamController - 當(dāng)你不再需要StreamController時(shí),關(guān)閉它;
  • 這同樣適用于RxDart主題,當(dāng)你不再需要BehaviourSubject,PublishSubject ...時(shí),請(qǐng)將其關(guān)閉。

如何基于由Stream提供的數(shù)據(jù)構(gòu)建Widget?(重點(diǎn))

Flutter提供了一個(gè)非常方便的StatefulWidget,名為StreamBuilder。

StreamBuilder監(jiān)聽(tīng)Stream,每當(dāng)某些數(shù)據(jù)輸出Stream時(shí),它會(huì)自動(dòng)重建,調(diào)用其builder callback。

這是如何使用StreamBuilder:

StreamBuilder<T>(
    key: ...可選...
    stream: ...需要監(jiān)聽(tīng)的stream...
    initialData: ...初始數(shù)據(jù),否則為空...
    builder: (BuildContext context, AsyncSnapshot<T> snapshot){
        if (snapshot.hasData){
            return ...基于snapshot.hasData返回的控件
        }
        return ...沒(méi)有數(shù)據(jù)的時(shí)候返回的控件
    },
)

以下示例模仿默認(rèn)的 “計(jì)數(shù)器” 應(yīng)用程序,但使用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(
      // 我們正在監(jiān)聽(tīng)流,每次有一個(gè)新值流出這個(gè)流時(shí),我們用該值更新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: (){
        //當(dāng)我們點(diǎn)擊FloatingActionButton時(shí),增加計(jì)數(shù)器并通過(guò)sink將其發(fā)送到Stream;
        //事實(shí)上 注入到stream中值會(huì)導(dǎo)致監(jiān)聽(tīng)它(stream)的StreamBuilder重建并 ‘刷新’計(jì)數(shù)器;
          _streamController.sink.add(++_counter);
        },
      ),
    );
  }
}
注意點(diǎn):
  • 24-30行: 我們不再需要state的概念,所有東西都通過(guò)Stream接受;

第35行:當(dāng)我們點(diǎn)擊FloatingActionButton時(shí),我們遞增計(jì)數(shù)器并通過(guò)接收器將其發(fā)送到Stream; 在流中注入值的事實(shí)導(dǎo)致偵聽(tīng)它的StreamBuilder重建并“刷新”計(jì)數(shù)器;

  • 這是一個(gè)很大的改進(jìn),因?yàn)閷?shí)際調(diào)用setState()方法的,會(huì)強(qiáng)制整個(gè) Widget(和任何子小部件)重建。這里,只有StreamBuilder被重建(當(dāng)然它的子部件,被streamBuilder包裹的子控件);

  • 我們?nèi)匀辉跒轫?yè)面使用StatefulWidget的唯一原因,僅僅是因?yàn)槲覀冃枰ㄟ^(guò)dispose方法第15行釋放StreamController ;

什么是反應(yīng)式編程?

反應(yīng)式編程是使用異步數(shù)據(jù)流進(jìn)行編程。
換句話(huà)說(shuō),任何東西比如從事件(例如點(diǎn)擊),變量的變化,消息,......到構(gòu)建請(qǐng)求,可能改變或發(fā)生的所有事件的所有內(nèi)容都將被傳送,由數(shù)據(jù)流觸發(fā)。

很明顯,所有這些意味著,通過(guò)反應(yīng)式編程,應(yīng)用程序:
  • 變得異步
  • 圍繞Streams和listeners的概念進(jìn)行架構(gòu)
  • 當(dāng)某事發(fā)生在某處(事件,變量的變化......)時(shí),會(huì)向Stream發(fā)送通知
  • 如果 "某人" 監(jiān)聽(tīng)該流(無(wú)論其在應(yīng)用程序中的任何位置),它將被通知并將采取適當(dāng)?shù)男袆?dòng).
組件之間不再存在緊密耦合。

簡(jiǎn)而言之,當(dāng)Widget向Stream發(fā)送內(nèi)容時(shí),該Widget 不再需要知道:

  • 接下來(lái)會(huì)發(fā)生什么
  • 誰(shuí)可能使用這些信息(沒(méi)有一個(gè),一個(gè)或幾個(gè)小部件......)
  • 可能使用此信息的地方(無(wú)處,同一屏幕,另一個(gè),幾個(gè)...)
  • 當(dāng)這些信息可能被使用時(shí)(幾乎是直接,幾秒鐘之后,永遠(yuǎn)不會(huì)......)
  • ...... Widget只關(guān)心自己的事業(yè),就是這樣!
乍一看,讀到這個(gè),這似乎會(huì)導(dǎo)致應(yīng)用程序“ 無(wú)法控制 ”,但正如我們將看到的,情況正好相反。它給你:
  • 構(gòu)建僅負(fù)責(zé)特定活動(dòng)的部分應(yīng)用程序的機(jī)會(huì)
  • 輕松模擬一些組件的行為,以允許更完整的測(cè)試覆蓋
  • 輕松重用組件(當(dāng)前應(yīng)用程序或其他應(yīng)用程序中的其他位置),
  • 重新設(shè)計(jì)應(yīng)用程序,并能夠在不進(jìn)行太多重構(gòu)的情況下將組件從一個(gè)地方移動(dòng)到另一個(gè)地方,

我們將很快看到優(yōu)勢(shì)......但在我需要介紹最后一個(gè)主題之前:BLoC模式。


BLoC 模式

BLoC模式由Paolo Soares 和 Cong Hui設(shè)計(jì),并谷歌在2018的 DartConf 首次提出,可以在 YouTube 上觀看。

BLoC表示為業(yè)務(wù)邏輯組件 (Business Logic Component)

簡(jiǎn)而言之, Business Logic需要:

  • 轉(zhuǎn)移到一個(gè)或幾個(gè)BLoC,
  • 盡可能從表示層(Presentation Layer)中刪除。換句話(huà)說(shuō),UI組件應(yīng)該只關(guān)心UI事物而不關(guān)心業(yè)務(wù)
  • 依賴(lài) Streams 獨(dú)家使用輸入(Sink)和輸出(stream)
  • 保持平臺(tái)獨(dú)立
  • 保持環(huán)境獨(dú)立

事實(shí)上,BLoC模式最初被設(shè)想為允許獨(dú)立于平臺(tái)重用相同的代碼:Web應(yīng)用程序,移動(dòng)應(yīng)用程序,后端。

它究竟意味著什么?

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

image.png
  • Widgets 通過(guò) Sinks 向 BLoC 發(fā)送事件(event)
  • BLoC 通過(guò)流(stream)通知小部件(widgets)
  • 由BLoC實(shí)現(xiàn)的業(yè)務(wù)邏輯不是他們關(guān)注的問(wèn)題。
從這個(gè)聲明中,我們可以直接看到一個(gè)巨大的好處。

由于業(yè)務(wù)邏輯與UI的分離:

  • 我們可以隨時(shí)更改業(yè)務(wù)邏輯,對(duì)應(yīng)用程序的影響最小
  • 我們可能會(huì)更改UI而不會(huì)對(duì)業(yè)務(wù)邏輯產(chǎn)生任何影響,
  • 現(xiàn)在,測(cè)試業(yè)務(wù)邏輯變得更加容易。
如何將此 BLoC 模式應(yīng)用于 Counter 應(yīng)用程序示例中

將 BLoC 模式應(yīng)用于此計(jì)數(shù)器應(yīng)用程序似乎有點(diǎn)矯枉過(guò)正,但讓我先向你展示......

代碼: 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來(lái)處理計(jì)數(shù)器
  //
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  //
  //  Stream來(lái)處理計(jì)數(shù)器上的操作
  //
  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);
  }
}

我已經(jīng)聽(tīng)到你說(shuō)“ 哇......為什么這一切?這都是必要的嗎?”。

第一 是責(zé)任分離
如果你檢查CounterPage(第21-45行),其中絕對(duì)沒(méi)有任何業(yè)務(wù)邏輯。

此頁(yè)面現(xiàn)在僅負(fù)責(zé):

> * 顯示計(jì)數(shù)器,現(xiàn)在只在必要時(shí)刷新(即使沒(méi)有頁(yè)面必須知道它)
> * 提供按鈕,當(dāng)按下時(shí),將會(huì)在counter面板上請(qǐng)求一個(gè)動(dòng)作

此外,整個(gè)業(yè)務(wù)邏輯集中在一個(gè)單獨(dú)的類(lèi)“ IncrementBloc”中。

如果現(xiàn)在,你需要更改業(yè)務(wù)邏輯,你只需更新方法_handleLogic(第77-80行)。也許新的業(yè)務(wù)邏輯將要求做非常復(fù)雜的事情...... CounterPage永遠(yuǎn)不會(huì)知道它,這是非常好的!
第二 可測(cè)試性
現(xiàn)在,測(cè)試業(yè)務(wù)邏輯變得更加容易。

無(wú)需再通過(guò)用戶(hù)界面測(cè)試業(yè)務(wù)邏輯。只需要測(cè)試IncrementBloc類(lèi)。
第三 自由組織布局
由于使用了Streams,你現(xiàn)在可以獨(dú)立于業(yè)務(wù)邏輯組織布局。

可以從應(yīng)用程序中的任何位置啟動(dòng)任何操作:只需調(diào)用.incrementCounter sink即可。

你可以在任何頁(yè)面的任何位置顯示計(jì)數(shù)器,只需聽(tīng)取.outCounter stream。
第四 減少“build”的次數(shù)
不使用setState()而是使用StreamBuilder這一事實(shí)大大減少了“ 構(gòu)建 ”的次數(shù),只減少了所需的次數(shù)。

從性能角度來(lái)看,這是一個(gè)巨大的進(jìn)步。
只有一個(gè)約束...... BLoC的可訪(fǎng)問(wèn)性

為了讓所有這些工作,BLoC需要可訪(fǎng)問(wèn)。

有幾種方法可以訪(fǎng)問(wèn)它:

  • 通過(guò)全局單例
    這種方式很有簡(jiǎn)單,但不是真的推薦。此外,由于Dart中沒(méi)有類(lèi)析構(gòu)函數(shù),因此你永遠(yuǎn)無(wú)法正確釋放資源。

  • 作為局部變量(本地實(shí)例)
    你可以實(shí)例化BLoC的本地實(shí)例。在某些情況下,此解決方案完全符合某些需求。在這種情況下,你應(yīng)該始終考慮在StatefulWidget中初始化,以便你可以利用dispose()方法來(lái)釋放它。

  • 由父類(lèi)提供
    使其可訪(fǎng)問(wèn)的最常見(jiàn)方式是通過(guò)祖先 Widget,實(shí)現(xiàn)為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;
  }
}
關(guān)于這種通用BlocProvider的一些解釋

首先,如何將其作為provider使用?

如果你查看示例代碼“ streams_4.dart ”,你將看到以下代碼行(第12-15行)

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

通過(guò)這些代碼,我們只需實(shí)例化一個(gè)新的BlocProvider,它將處理一個(gè)IncrementBloc,并將CounterPage作為子項(xiàng)呈現(xiàn)。

從那一刻開(kāi)始,從BlocProvider開(kāi)始的子樹(shù)的任何小部件部分都將能夠通過(guò)以下代碼訪(fǎng)問(wèn)IncrementBloc:

IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
可以使用多個(gè)BLoC嗎?

當(dāng)然,這是非常可取的。建議是:

  • (如果有任何業(yè)務(wù)邏輯)每頁(yè)頂部有一個(gè)BLoC,
  • 為什么不是ApplicationBloc來(lái)處理應(yīng)用程序狀態(tài)?
  • 每個(gè)“足夠復(fù)雜的組件”都有相應(yīng)的BLoC。

以下示例代碼在整個(gè)應(yīng)用程序的頂部顯示ApplicationBloc,然后在CounterPage頂部顯示IncrementBloc。

該示例還顯示了如何檢索兩個(gè)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相關(guān)的大多數(shù)文章中,你會(huì)看到通過(guò)InheritedWidget實(shí)現(xiàn)Provider。

當(dāng)然,沒(méi)有什么能阻止這種類(lèi)型的實(shí)現(xiàn)。然而,

  • 一個(gè)InheritedWidget沒(méi)有提供任何dispose方法,記住,在不再需要資源時(shí)總是釋放資源是一個(gè)很好的做法。
  • 當(dāng)然,沒(méi)有什么能阻止你將InheritedWidget包裝在另一個(gè)StatefulWidget中,但是,使用 InheritedWidget 增加了什么呢?
  • 最后,如果不受控制,使用InheritedWidget經(jīng)常會(huì)導(dǎo)致副作用(請(qǐng)參閱下面的InheritedWidget上的提醒)。

以上三點(diǎn)解釋了我為什么選擇通過(guò)StatefulWidget實(shí)現(xiàn)BlocProvider,這樣做可以讓我在Widget dispose時(shí)釋放相關(guān)資源。

Flutter無(wú)法實(shí)例化泛型類(lèi)型
不幸的是,F(xiàn)lutter無(wú)法實(shí)例化泛型類(lèi)型,我們必須將BLoC的實(shí)例傳遞給BlocProvider。為了在每個(gè)BLoC中強(qiáng)制執(zhí)行dispose()方法,所有BLoC都必須實(shí)現(xiàn)BlocBase接口。

提醒InheritedWidget

在使用InheritedWidget并通過(guò)context.inheritFromWidgetOfExactType(...)來(lái)獲得指定類(lèi)型最近的widget, 每次InheritedWidget的父級(jí)或者子布局發(fā)生變化時(shí),這個(gè)方法會(huì)自動(dòng)將當(dāng)前“context”(= BuildContext)注冊(cè)到要重建的widget當(dāng)中。。

請(qǐng)注意,為了完全正確,我剛才解釋的與InheritedWidget相關(guān)的問(wèn)題只發(fā)生在我們將InheritedWidget與StatefulWidget結(jié)合使用時(shí)。當(dāng)你只使用沒(méi)有State的InheritedWidget時(shí),問(wèn)題就不會(huì)發(fā)生。但是......我將在下一篇文章 中回到這句話(huà)。

鏈接到BuildContext的Widget類(lèi)型(Stateful或Stateless)無(wú)關(guān)緊要。

關(guān)于BLoC的個(gè)人建議

與BLoC相關(guān)的第三條規(guī)則是:“依賴(lài)于Streams的輸入(Sink)和輸出(stream)的使用優(yōu)勢(shì)”。

我的個(gè)人經(jīng)歷稍微關(guān)系到這個(gè)說(shuō)法......讓我解釋一下。

首先,BLoC模式被設(shè)想為跨平臺(tái)共享相同的代碼(AngularDart,......),并且從這個(gè)角度來(lái)看,該陳述完全有意義。

但是,如果你只打算開(kāi)發(fā)一個(gè)Flutter應(yīng)用程序,這是基于我的謙遜經(jīng)驗(yàn),有點(diǎn)矯枉過(guò)正。

如果我們堅(jiān)持聲明,沒(méi)有可能的getter或setter,只有sink和stream。缺點(diǎn)是“所有這些都是異步的”。

讓我們用2個(gè)樣本來(lái)說(shuō)明缺點(diǎn):

你需要從BLoC中檢索一些數(shù)據(jù),以便將這些數(shù)據(jù)用作應(yīng)該立即顯示這些參數(shù)的頁(yè)面的輸入(例如,想一個(gè)參數(shù)頁(yè)面),如果我們不得不依賴(lài)Streams,這使得頁(yè)面的構(gòu)建異步(這很復(fù)雜)。通過(guò)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()級(jí)別尚未提供的上下文,如果尚未初始化,我們將獲得過(guò)濾器參數(shù)列表
    if (_isInit == false){
      _movieBloc = BlocProvider.of<MovieCatalogBloc>(context);
      _getFilterParameters();
    }
  }

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

  ///
  /// 非常棘手.
  /// 
  /// 由于我們希望100%符合BLoC標(biāo)準(zhǔn),我們需要使用Streams從BLoCs中檢索所有內(nèi)容......
  /// 
  /// 這很難看,但被視為一個(gè)研究案例。
  ///
  void _getFilterParameters() {
    StreamSubscription subscriptionFilters;

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

        // 只需確保訂閱已發(fā)布
        subscriptionFilters.cancel();
        
        // 現(xiàn)在我們有了所有參數(shù),我們可以構(gòu)建實(shí)際的頁(yè)面
        if (mounted){
          setState((){
            _isInit = true;
          });
        }
      });
    });
  }
}

在BLoC級(jí)別,您還需要轉(zhuǎn)換某些數(shù)據(jù)的“假”注入,以觸發(fā)提供您希望通過(guò)流接收的數(shù)據(jù)。使這項(xiàng)工作的示例代碼可以是:

代碼streams_8.dart

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

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

  ApplicationBloc() {
    //
    // 如果我們通過(guò)此接收器接收任何數(shù)據(jù),我們只需將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);

我不知道你的意見(jiàn),但就個(gè)人而言,如果我沒(méi)有任何與代碼移植/共享相關(guān)的限制,我發(fā)現(xiàn)這太重了,我寧愿在需要時(shí)使用常規(guī)的getter / setter并使用Streams / Sinks來(lái)保持分離責(zé)任并在需要的地方廣播信息,這很棒。

現(xiàn)在是時(shí)候在實(shí)踐中看到這一切......

正如本文開(kāi)頭所提到的,我構(gòu)建了一個(gè)偽應(yīng)用程序來(lái)展示如何使用所有這些概念。 完整的源代碼可以在 Github 上找到。

請(qǐng)諒解,因?yàn)檫@段代碼遠(yuǎn)非完美,可能更好和/或更好的架構(gòu),但唯一的目標(biāo)只是向您展示這一切是如何工作的。

由于源代碼太多很多,我只會(huì)解釋主要的幾條。

電影目錄的來(lái)源

我使用免費(fèi)的TMDB API來(lái)獲取所有電影的列表,以及海報(bào),評(píng)級(jí)和描述。

為了能夠運(yùn)行此示例應(yīng)用程序,您需要注冊(cè)并獲取API密鑰(完全免費(fèi)),然后將您的API密鑰放在文件“/api/tmdb_api.dart”第15行。

應(yīng)用程序的架構(gòu)如下:

該應(yīng)用程序使用到了:

3個(gè)主要的BLoC:

    1. ApplicationBloc(在所有內(nèi)容之上),負(fù)責(zé)提供所有電影類(lèi)型的列表;
  • 2.FavoriteBloc(就在下面),負(fù)責(zé)處理“收藏夾”的概念;
  • 3.MovieCatalogBloc(在2個(gè)主要頁(yè)面之上),負(fù)責(zé)根據(jù)過(guò)濾器提供電影列表;

6個(gè)頁(yè)面:

  • 1.HomePage:登陸頁(yè)面,允許導(dǎo)航到3個(gè)子頁(yè)面;
  • 2.ListPage:將電影列為GridView的頁(yè)面,允許過(guò)濾,收藏夾選擇,訪(fǎng)問(wèn)收藏夾以及在后續(xù)頁(yè)面中顯示電影詳細(xì)信息;
  • 3.ListOnePage:類(lèi)似于ListPage,但電影列表顯示為水平列表,下面是詳細(xì)信息;
    1. FavoritesPage:列出收藏夾的頁(yè)面,允許取消選擇任何收藏夾;
  • 5.* Filters:允許定義過(guò)濾器的EndDrawer:流派和最小/最大發(fā)布日期。從ListPage或ListOnePage調(diào)用此頁(yè)面;
  1. Details*詳細(xì)信息:頁(yè)面僅由ListPage調(diào)用以顯示電影的詳細(xì)信息,但也允許選擇/取消選擇電影作為收藏;

1個(gè)子BLoC:

  • 1.FavoriteMovieBloc,鏈接到MovieCardWidget或MovieDetailsWidget,以處理作為收藏的電影的選擇/取消選擇

5個(gè)主要Widget:

  • 1.FavoriteButton:負(fù)責(zé)顯示收藏夾的數(shù)量,實(shí)時(shí),并在按下時(shí)重定向到FavoritesPage;
  • 2.FavoriteWidget:負(fù)責(zé)顯示一個(gè)喜歡的電影的細(xì)節(jié)并允許其取消選擇;
  • 3.FiltersSummary:負(fù)責(zé)顯示當(dāng)前定義的過(guò)濾器;
  • 4.MovieCardWidget:負(fù)責(zé)將一部電影顯示為卡片,電影海報(bào),評(píng)級(jí)和名稱(chēng),以及一個(gè)圖標(biāo),表示該特定電影的選擇是最喜歡的;
  • 5.MovieDetailsWidget:負(fù)責(zé)顯示與特定電影相關(guān)的詳細(xì)信息,并允許其選擇/取消選擇作為收藏。
不同BLoCs / Streams的編排

下圖顯示了如何使用主要3個(gè)BLoC:

  • 在BLoC的左側(cè),哪些組件調(diào)用Sink
  • 在右側(cè),哪些組件監(jiān)聽(tīng)流

例如,當(dāng)MovieDetailsWidget調(diào)用inAddFavorite Sink時(shí),會(huì)觸發(fā)2個(gè)stream:

  • outTotalFavorites流強(qiáng)制重建FavoriteButton
  • outFavorites流
    強(qiáng)制重建MovieDetailsWidget(“最喜歡的”圖標(biāo))
    強(qiáng)制重建_buildMoieCard(“最喜歡的”圖標(biāo))
    用于構(gòu)建每個(gè)MovieDetailsWidget
image.png
觀察

大多數(shù)Widget和Page都是StatelessWidgets,這意味著:

  • 強(qiáng)制重建的setState()幾乎從未使用過(guò)。 例外情況是:
    在ListOnePage中,當(dāng)用戶(hù)點(diǎn)擊MovieCard時(shí),刷新MovieDetailsWidget。 這也可能是由一個(gè)stream驅(qū)動(dòng)的......
    在FiltersPage中允許用戶(hù)在接受篩選條件之前通過(guò)Sink更改過(guò)篩選條件。
  • 應(yīng)用程序不使用任何InheritedWidget
  • 該應(yīng)用程序幾乎是100%BLoCs / Streams驅(qū)動(dòng),這意味著大多數(shù)小部件彼此獨(dú)立,并且它們?cè)趹?yīng)用程序中的位置

一個(gè)實(shí)際的例子是FavoriteButton,它顯示徽章中所選收藏夾的數(shù)量。 該應(yīng)用程序共有3個(gè)FavoriteButton實(shí)例,每個(gè)實(shí)例顯示在3個(gè)不同的頁(yè)面中。

顯示電影列表(顯示無(wú)限列表的技巧說(shuō)明)

要顯示符合過(guò)濾條件的電影列表,我們使用GridView.builder(ListPage)或ListView.builder(ListOnePage)作為無(wú)限滾動(dòng)列表。

電影是通過(guò)TMDB API獲取的,每次拉取20個(gè)。

提醒一下,GridView.builder和ListView.builder都將itemCount作為輸入,如果提供了item數(shù)量,則表示要根據(jù)itemCount的數(shù)量來(lái)顯示列表。itemBuilder的index從0到itemCount - 1不等。

正如您將在代碼中看到的那樣,我隨意為GridView.builder添加了30多個(gè)。 理由是,在這個(gè)例子中,我們正在操縱假定的無(wú)限數(shù)量的項(xiàng)目(這不是完全正確但是又有誰(shuí)關(guān)心這個(gè)例子)。 這將強(qiáng)制GridView.builder請(qǐng)求顯示“最多30個(gè)”項(xiàng)目。

此外,GridView.builder和ListView.builder只在認(rèn)為必須在視口中呈現(xiàn)某個(gè)項(xiàng)目(索引)時(shí)才調(diào)用itemBuilder。

MovieCatalogBloc.outMoviesList返回一個(gè)List <MovieCard>,它被迭代以構(gòu)建每個(gè)Movie Card。 第一次,這個(gè)List <MovieCard>是空的,但是由于itemCount:... + 30,我們欺騙系統(tǒng),它將要求通過(guò)_buildMovieCard(...)呈現(xiàn)30個(gè)不存在的項(xiàng)目。

正如您將在代碼中看到的,此例程對(duì)Sink進(jìn)行了一次奇怪的調(diào)用:

//通知MovieCatalogBloc我們正在渲染MovieCard[index]
movieBloc.inMovieIndex.add(index);

這個(gè)調(diào)用告訴MovieCatalogBloc我們要渲染MovieCard [index]。

然后_buildMovieCard(...)繼續(xù)驗(yàn)證與MovieCard [index]相關(guān)的數(shù)據(jù)是否存在。 如果是,則渲染后者,否則顯示CircularProgressIndicator。

對(duì)StreamCatalogBloc.inMovieIndex.add(index)的調(diào)用由StreamSubscription監(jiān)聽(tīng),StreamSubscription將索引轉(zhuǎn)換為某個(gè)pageIndex數(shù)字(一頁(yè)最多可計(jì)20部電影)。 如果尚未從TMDB API獲取相應(yīng)頁(yè)面,則會(huì)調(diào)用API。 獲取頁(yè)面后,所有已獲取電影的新列表將發(fā)送到_moviesController。 當(dāng)GridView.builder監(jiān)聽(tīng)該Stream(= movieBloc.outMoviesList)時(shí),后者請(qǐng)求重建相應(yīng)的MovieCard。 由于我們現(xiàn)在擁有數(shù)據(jù),我們可以渲染它了。

名單和其他鏈接
介紹PublishSubject,BehaviorSubject和ReplaySubject的圖片由ReactiveX發(fā)布。
其他一些有趣的文章值得一讀:

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]

總結(jié)

很長(zhǎng)的文章,但還有更多的話(huà)要說(shuō),因?yàn)閷?duì)我而言,這是展開(kāi)Flutter應(yīng)用程序的方法。 它提供了很大的靈活性。

很快就會(huì)繼續(xù)關(guān)注新文章。 快樂(lè)寫(xiě)代碼。

這篇文章也可以在 Medium -Flutter Community 找到。

本文源碼

如需轉(zhuǎn)載本譯文,請(qǐng)注明出處.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容