Flutter - BLoC 第一講

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

【譯文】Reactive Programming - Streams - BLoC

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

[toc]

本譯文介紹Streams、Bloc 和 Reactive Programming 的概念溉愁。理論和實(shí)踐范例处铛。對(duì)于作者的個(gè)人note沒有進(jìn)行翻譯,請(qǐng)自行翻閱原文地址 原文原碼拐揭。和iOS開發(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中

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

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

Stream可以傳遞什么彻磁?

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

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

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

定義監(jiān)聽器時(shí)衍锚,你會(huì)收到StreamSubscription對(duì)象友题。通過StreamSubscription對(duì)象,你將收到由Stream發(fā)生變化而觸發(fā)通知构拳。

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

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

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

  • 停止聽
  • 暫停凫海,
  • 恢復(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)行任何類型的處理啰脚,例如:

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

Stream流的類型

Stream有兩種類型匀们。

單訂閱Stream

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

即在第一個(gè)訂閱被取消后准给,也無法在此類流上收聽兩次泄朴。

廣播流

第二種類型的Stream允許任意數(shù)量的監(jiān)聽器重抖。

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

基本的例子

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

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

streams_1.dart

import 'dart:async';

void main() {
  //
  // 初始化“單訂閱”流控制器
  //
  final StreamController ctrl = StreamController();
  
  //
   //初始化一個(gè)只打印數(shù)據(jù)的監(jiān)聽器
  //一收到它
  //
  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來過濾(第14行)值,只讓偶數(shù)經(jīng)過陌兑。

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

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

PublishSubject

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


image.png

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

BehaviorSubject

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

image.png

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

ReplaySubject

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

image.png

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

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

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

  • StreamSubscription - 當(dāng)你不再需要監(jiān)聽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)聽Stream罚随,每當(dāng)某些數(shù)據(jù)輸出Stream時(shí)玉工,它會(huì)自動(dòng)重建,調(diào)用其builder callback淘菩。

這是如何使用StreamBuilder:

StreamBuilder<T>(
    key: ...可選...
    stream: ...需要監(jiān)聽的stream...
    initialData: ...初始數(shù)據(jù)遵班,否則為空...
    builder: (BuildContext context, AsyncSnapshot<T> snapshot){
        if (snapshot.hasData){
            return ...基于snapshot.hasData返回的控件
        }
        return ...沒有數(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)聽流狭郑,每次有一個(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ù)器并通過sink將其發(fā)送到Stream翰萨;
        //事實(shí)上 注入到stream中值會(huì)導(dǎo)致監(jiān)聽它(stream)的StreamBuilder重建并 ‘刷新’計(jì)數(shù)器;
          _streamController.sink.add(++_counter);
        },
      ),
    );
  }
}
注意點(diǎn):
  • 24-30行: 我們不再需要state的概念,所有東西都通過Stream接受;

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

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

  • 我們?nèi)匀辉跒轫撁媸褂肧tatefulWidget的唯一原因羡洁,僅僅是因?yàn)槲覀冃枰ㄟ^dispose方法第15行釋放StreamController ;

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

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

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

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

  • 接下來會(huì)發(fā)生什么
  • 誰可能使用這些信息(沒有一個(gè),一個(gè)或幾個(gè)小部件......)
  • 可能使用此信息的地方(無處榜跌,同一屏幕,另一個(gè)盅粪,幾個(gè)...)
  • 當(dāng)這些信息可能被使用時(shí)(幾乎是直接钓葫,幾秒鐘之后,永遠(yuǎn)不會(huì)......)
  • ...... Widget只關(guān)心自己的事業(yè)票顾,就是這樣础浮!
乍一看,讀到這個(gè)奠骄,這似乎會(huì)導(dǎo)致應(yīng)用程序“ 無法控制 ”霸旗,但正如我們將看到的,情況正好相反戚揭。它給你:
  • 構(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)中刪除宵晚。換句話說垂攘,UI組件應(yīng)該只關(guān)心UI事物而不關(guān)心業(yè)務(wù)
  • 依賴 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 通過 Sinks 向 BLoC 發(fā)送事件(event)
  • BLoC 通過流(stream)通知小部件(widgets)
  • 由BLoC實(shí)現(xiàn)的業(yè)務(wù)邏輯不是他們關(guā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)矯枉過正,但讓我先向你展示......

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

  //
  //  Stream來處理計(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)聽到你說“ 哇......為什么這一切疟丙?這都是必要的嗎猾警?”孔祸。

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

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

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

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

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

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

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

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

從性能角度來看痢虹,這是一個(gè)巨大的進(jìn)步。
只有一個(gè)約束...... BLoC的可訪問性

為了讓所有這些工作主儡,BLoC需要可訪問世分。

有幾種方法可以訪問它:

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

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

  • 由父類提供
    使其可訪問的最常見方式是通過祖先 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(),
        ),

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

從那一刻開始脆贵,從BlocProvider開始的子樹的任何小部件部分都將能夠通過以下代碼訪問IncrementBloc:

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

當(dāng)然起暮,這是非陈舭保可取的。建議是:

  • (如果有任何業(yè)務(wù)邏輯)每頁頂部有一個(gè)BLoC负懦,
  • 為什么不是ApplicationBloc來處理應(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ì)看到通過InheritedWidget實(shí)現(xiàn)Provider贫导。

當(dāng)然抛猫,沒有什么能阻止這種類型的實(shí)現(xiàn)。然而孩灯,

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

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

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

提醒InheritedWidget

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

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

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

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

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

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

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

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

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

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

你需要從BLoC中檢索一些數(shù)據(jù)魔市,以便將這些數(shù)據(jù)用作應(yīng)該立即顯示這些參數(shù)的頁面的輸入(例如主届,想一個(gè)參數(shù)頁面),如果我們不得不依賴Streams待德,這使得頁面的構(gòu)建異步(這很復(fù)雜)岂膳。通過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í)別尚未提供的上下文磅网,如果尚未初始化谈截,我們將獲得過濾器參數(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í)際的頁面
        if (mounted){
          setState((){
            _isInit = true;
          });
        }
      });
    });
  }
}

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

代碼streams_8.dart

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

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

  ApplicationBloc() {
    //
    // 如果我們通過此接收器接收任何數(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);

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

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

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

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

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

電影目錄的來源

我使用免費(fèi)的TMDB API來獲取所有電影的列表慎璧,以及海報(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é)提供所有電影類型的列表;
  • 2.FavoriteBloc(就在下面),負(fù)責(zé)處理“收藏夾”的概念;
  • 3.MovieCatalogBloc(在2個(gè)主要頁面之上)蚯姆,負(fù)責(zé)根據(jù)過濾器提供電影列表;

6個(gè)頁面:

  • 1.HomePage:登陸頁面五续,允許導(dǎo)航到3個(gè)子頁面;
  • 2.ListPage:將電影列為GridView的頁面,允許過濾龄恋,收藏夾選擇疙驾,訪問收藏夾以及在后續(xù)頁面中顯示電影詳細(xì)信息;
  • 3.ListOnePage:類似于ListPage,但電影列表顯示為水平列表郭毕,下面是詳細(xì)信息;
    1. FavoritesPage:列出收藏夾的頁面它碎,允許取消選擇任何收藏夾;
  • 5.* Filters:允許定義過濾器的EndDrawer:流派和最小/最大發(fā)布日期。從ListPage或ListOnePage調(diào)用此頁面;
  1. Details*詳細(xì)信息:頁面僅由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)前定義的過濾器;
  • 4.MovieCardWidget:負(fù)責(zé)將一部電影顯示為卡片金拒,電影海報(bào),評(píng)級(jí)和名稱套腹,以及一個(gè)圖標(biāo)绪抛,表示該特定電影的選擇是最喜歡的;
  • 5.MovieDetailsWidget:負(fù)責(zé)顯示與特定電影相關(guān)的詳細(xì)信息,并允許其選擇/取消選擇作為收藏电禀。
不同BLoCs / Streams的編排

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

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

例如鞭呕,當(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()幾乎從未使用過葫松。 例外情況是:
    在ListOnePage中,當(dāng)用戶點(diǎn)擊MovieCard時(shí)瓦糕,刷新MovieDetailsWidget。 這也可能是由一個(gè)stream驅(qū)動(dòng)的......
    在FiltersPage中允許用戶在接受篩選條件之前通過Sink更改過篩選條件腋么。
  • 應(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è)不同的頁面中摧扇。

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

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

電影是通過TMDB API獲取的扛稽,每次拉取20個(gè)吁峻。

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

正如您將在代碼中看到的那樣啄骇,我隨意為GridView.builder添加了30多個(gè)。 理由是瘟斜,在這個(gè)例子中缸夹,我們正在操縱假定的無限數(shù)量的項(xiàng)目(這不是完全正確但是又有誰關(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)亲雪,它將要求通過_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)聽傀蚌,StreamSubscription將索引轉(zhuǎn)換為某個(gè)pageIndex數(shù)字(一頁最多可計(jì)20部電影)基显。 如果尚未從TMDB API獲取相應(yīng)頁面,則會(huì)調(diào)用API善炫。 獲取頁面后撩幽,所有已獲取電影的新列表將發(fā)送到_moviesController。 當(dāng)GridView.builder監(jiān)聽該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)的文章静汤,但還有更多的話要說琅催,因?yàn)閷?duì)我而言,這是展開Flutter應(yīng)用程序的方法撒妈。 它提供了很大的靈活性恢暖。

很快就會(huì)繼續(xù)關(guān)注新文章排监。 快樂寫代碼狰右。

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

本文源碼

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末棋蚌,一起剝皮案震驚了整個(gè)濱河市嫁佳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谷暮,老刑警劉巖蒿往,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異湿弦,居然都是意外死亡瓤漏,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門颊埃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔬充,“玉大人,你說我怎么就攤上這事班利〖⒙” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵罗标,是天一觀的道長(zhǎng)庸队。 經(jīng)常有香客問我,道長(zhǎng)闯割,這世上最難降的妖魔是什么彻消? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮宙拉,結(jié)果婚禮上证膨,老公的妹妹穿的比我還像新娘。我一直安慰自己鼓黔,他們只是感情好央勒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著澳化,像睡著了一般崔步。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缎谷,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天井濒,我揣著相機(jī)與錄音,去河邊找鬼列林。 笑死瑞你,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的希痴。 我是一名探鬼主播者甲,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼砌创!你這毒婦竟也來了虏缸?” 一聲冷哼從身側(cè)響起鲫懒,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刽辙,沒想到半個(gè)月后窥岩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宰缤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年颂翼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慨灭。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡疚鲤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缘挑,到底是詐尸還是另有隱情集歇,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布语淘,位于F島的核電站诲宇,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏惶翻。R本人自食惡果不足惜姑蓝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吕粗。 院中可真熱鬧纺荧,春花似錦、人聲如沸颅筋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽议泵。三九已至占贫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間先口,已是汗流浹背型奥。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碉京,地道東北人厢汹。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像谐宙,于是被迫代替她去往敵國(guó)和親烫葬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355