本篇已同步到 個(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)聽StreamController 的stream屬性。
定義監(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。
與PublishSubject的主要區(qū)別在于BehaviorSubject還將最后發(fā)送的事件發(fā)送給剛剛訂閱的監(jiān)聽器锅减。
ReplaySubject
ReplaySubject 也是一個(gè)廣播StreamController糖儡,它返回一個(gè)Observable,而不是Stream怔匣。
默認(rèn)情況下握联,ReplaySubject將Stream已經(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 (流)
- 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使其工作的示例代碼可能如下所示......很丑陋不是嗎。
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)工作的示例代碼可以是:
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:
- 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ì)信息;
- FavoritesPage:列出收藏夾的頁面它碎,允許取消選擇任何收藏夾;
- 5.* Filters:允許定義過濾器的EndDrawer:流派和最小/最大發(fā)布日期。從ListPage或ListOnePage調(diào)用此頁面;
- 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
觀察
大多數(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)注明出處.