本篇已同步到 個(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)StreamController 的stream屬性。
定義監(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。

與PublishSubject的主要區(qū)別在于BehaviorSubject還將最后發(fā)送的事件發(fā)送給剛剛訂閱的監(jiān)聽(tīng)器。
ReplaySubject
ReplaySubject 也是一個(gè)廣播StreamController,它返回一個(gè)Observable,而不是Stream。

默認(rèn)情況下,ReplaySubject將Stream已經(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 (流)

- 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使其工作的示例代碼可能如下所示......很丑陋不是嗎。
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)工作的示例代碼可以是:
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:
- 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ì)信息;
- FavoritesPage:列出收藏夾的頁(yè)面,允許取消選擇任何收藏夾;
- 5.* Filters:允許定義過(guò)濾器的EndDrawer:流派和最小/最大發(fā)布日期。從ListPage或ListOnePage調(diào)用此頁(yè)面;
- 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

觀察
大多數(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)注明出處.
