本篇已同步到 個人博客 ,歡迎常來。
[譯文]Reactive Programming - Streams - BLoC實際用例 原文
BLoC羹奉,Reactive Programming秒旋,Streams - 實際用例和有用模式。
[TOC]
注:此處的"toc"應(yīng)顯示為目錄诀拭,但是簡書不支持迁筛,顯示不出來。
介紹
在介紹了BLoC炫加,Reactive Programming和Streams的概念后瑰煎,我在一段時間之前做了一些介紹铺然,盡管與我分享一些我經(jīng)常使用并且個人覺得非常有用的模式(至少對我而言)可能會很有趣俗孝。這些模式使我在開發(fā)過程中節(jié)省了大量時間,并使我的代碼更易于閱讀和調(diào)試魄健。
我要談的話題是:
- 1.BLoC Provider and InheritedWidget
- 2.在哪里初始化BLoC赋铝?
- 3.事件狀態(tài)(允許根據(jù)事件響應(yīng)狀態(tài)轉(zhuǎn)換)
- 4.表格驗證(允許根據(jù)條目和驗證控制表單的行為)
- 5.Part Of(允許Widget根據(jù)其在列表中的存在來調(diào)整其行為)
完整的源代碼可以在GitHub上找到。
1.BLoC Provider and InheritedWidget
我借此文章的機會介紹我的BlocProvider的另一個版本沽瘦,它現(xiàn)在依賴于一個InheritedWidget革骨。
使用InheritedWidget的優(yōu)點是我們獲得了性能。
1.1. 之前的實現(xiàn)
我之前版本的BlocProvider實現(xiàn)為常規(guī)StatefulWidget析恋,如下所示:
abstract class BlocBase {
void dispose();
}
// Generic BLoC provider
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;
}
}
我使用StatefulWidget從dispose()方法中受益良哲,以確保在不再需要時釋放BLoC分配的資源。
這很好用但從性能角度來看并不是最佳的助隧。
context.ancestorWidgetOfExactType()是一個為時間復(fù)雜度為O(n)的函數(shù)筑凫,為了檢索某種類型的祖先,它將對widget樹 做向上導(dǎo)航并村,從上下文開始巍实,一次遞增一個父,直到完成哩牍。如果從上下文到祖先的距離很信锪省(即O(n)結(jié)果很少),則可以接受對此函數(shù)的調(diào)用膝昆,否則應(yīng)該避免丸边。這是這個函數(shù)的代碼。
@override
Widget ancestorWidgetOfExactType(Type targetType) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element ancestor = _parent;
while (ancestor != null && ancestor.widget.runtimeType != targetType)
ancestor = ancestor._parent;
return ancestor?.widget;
}
1.2. 新的實現(xiàn)
新實現(xiàn)依賴于StatefulWidget荚孵,并結(jié)合InheritedWidget:
Type _typeOf<T>() => T;
abstract class BlocBase {
void dispose();
}
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}): super(key: key);
final Widget child;
final T bloc;
@override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.bloc;
}
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>>{
@override
void dispose(){
widget.bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return new _BlocProviderInherited<T>(
bloc: widget.bloc,
child: widget.child,
);
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
_BlocProviderInherited({
Key key,
@required Widget child,
@required this.bloc,
}) : super(key: key, child: child);
final T bloc;
@override
bool updateShouldNotify(_BlocProviderInherited oldWidget) => false;
}
優(yōu)點是這個解決方案是性能妹窖。
由于使用了InheritedWidget,它現(xiàn)在可以調(diào)用context.ancestorInheritedElementForWidgetOfExactType()函數(shù)处窥,它是一個O(1)嘱吗,這意味著祖先的檢索是立即的,如其源代碼所示:
@override
InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null
? null
: _inheritedWidgets[targetType];
return ancestor;
}
這來自于所有InheritedWidgets都由Framework記憶的事實。
- 為什么使用 ancestorInheritedElementForWidgetOfExactType 谒麦?
- 您可能已經(jīng)注意到我使用 ancestorInheritedElementForWidgetOfExactType 方法而不是通常的 inheritFromWidgetOfExactType 俄讹。
- 原因是我不希望上下文調(diào)用的BlocProvider被注冊為InheritedWidget的依賴項,因為我不需要它绕德。
1.3. 如何使用新的BlocProvider患膛?
1.3.1.注入BLoC
Widget build(BuildContext context){
return BlocProvider<MyBloc>{
bloc: myBloc,
child: ...
}
}
1.3.2. 檢索BLoC
Widget build(BuildContext context){
MyBloc myBloc = BlocProvider.of<MyBloc>(context);
...
}
2.在哪里初始化BLoC?
要回答這個問題耻蛇,您需要弄清楚其使用范圍踪蹬。
2.1.應(yīng)用程序中隨處可用
假設(shè)您必須處理與用戶身份驗證/配置文件,用戶首選項臣咖,購物籃相關(guān)的一些機制, 可從應(yīng)用程序的任何可能部分(例如跃捣,從不同頁面)獲得獲得BLoC(),存在兩種方式使這個BLoC可訪問。
2.1.1.使用全局單例
import 'package:rxdart/rxdart.dart';
class GlobalBloc {
///
/// 與此BLoC相關(guān)的流
///
BehaviorSubject<String> _controller = BehaviorSubject<String>();
Function(String) get push => _controller.sink.add;
Stream<String> get stream => _controller;
///
/// Singleton工廠
///
static final GlobalBloc _bloc = new GlobalBloc._internal();
factory GlobalBloc(){
return _bloc;
}
GlobalBloc._internal();
///
/// Resource disposal
///
void dispose(){
_controller?.close();
}
GlobalBloc globalBloc = GlobalBloc();
要使用此BLoC夺蛇,您只需導(dǎo)入該類并直接調(diào)用其方法疚漆,如下所示:
import 'global_bloc.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
globalBloc.push('building MyWidget');
return Container();
}
}
這是一個可以接受的解決方案,如果你需要有一個BLoC是唯一的刁赦,需要從應(yīng)用程序內(nèi)的任意位置訪問娶聘。
- 這是非常容易使用;
- 它不依賴于任何BuildContext ;
- 沒有必要通過任何BlocProvider去尋找 BLoC;
- 為了釋放它的資源,只需確保將應(yīng)用程序?qū)崿F(xiàn)為StatefulWidget甚脉,并在應(yīng)用程序Widget 的重寫dispose()方法中調(diào)用globalBloc.dispose()
許多純粹主義者反對這種解決方案丸升。我不知道為什么,但是...所以讓我們看看另一個......
2.1.2. 把它放在一切之上
在Flutter中牺氨,所有頁面的祖先本身必須是MaterialApp的父級狡耻。這是由于這樣的事實,一個頁面(或路由)被包裝在一個OverlayEntry波闹,一個共同的孩子堆棧的所有頁面酝豪。
換句話說,每個頁面都有一個Buildcontext精堕,它獨立于任何其他頁面孵淘。這就解釋了為什么在不使用任何技巧的情況下,2頁(路線)不可能有任何共同點歹篓。
因此瘫证,如果您需要在應(yīng)用程序中的任何位置使用BLoC,則必須將其作為MaterialApp的父級庄撮,如下所示:
void main() => runApp(Application());
class Application extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<AuthenticationBloc>(
bloc: AuthenticationBloc(),
child: MaterialApp(
title: 'BLoC Samples',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: InitializationPage(),
),
);
}
}
2.2.可用于子樹
大多數(shù)情況下背捌,您可能需要在應(yīng)用程序的某些特定部分使用BLoC。
作為一個例子洞斯,我們可以想到的討論主題毡庆,其中集團(tuán)將用于
- 與服務(wù)器交互以檢索坑赡,添加,更新帖子
- 列出要在特定頁面中顯示的線程
- ...
因此么抗,如果您需要在應(yīng)用程序中的任何位置使用BLoC毅否,則必須將其作為MaterialApp的父級,如下所示:
class MyTree extends StatelessWidget {
@override
Widget build(BuildContext context){
return BlocProvider<MyBloc>(
bloc: MyBloc(),
child: Column(
children: <Widget>[
MyChildWidget(),
],
),
);
}
}
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
MyBloc = BlocProvider.of<MyBloc>(context);
return Container();
}
}
這樣一來蝇刀,所有widgets都可以通過對呼叫BlocProvider.of方法 訪問BLoC
附:
如上所示的解決方案并不是最佳解決方案螟加,因為它將在每次重建時實例化BLoC。
后果:
- 您將丟失任何現(xiàn)有的BLoC內(nèi)容
- 它會耗費CPU時間吞琐,因為它需要在每次構(gòu)建時實例化它捆探。
一個更好的辦法,在這種情況下站粟,是使用StatefulWidget從它的持久受益國黍图,具體如下:
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree>{
MyBloc bloc;
@override
void initState(){
super.initState();
bloc = MyBloc();
}
@override
void dispose(){
bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return BlocProvider<MyBloc>(
bloc: bloc,
child: Column(
children: <Widget>[
MyChildWidget(),
],
),
);
}
}
使用這種方法,如果需要重建“ MyTree ”小部件卒蘸,則不必重新實例化BLoC并直接重用現(xiàn)有實例雌隅。
2.3.僅適用于一個小部件
這涉及BLoC僅由一個 Widget使用的情況。
在這種情況下缸沃,可以在Widget中實例化BLoC。
3.事件狀態(tài)(允許根據(jù)事件響應(yīng)狀態(tài)轉(zhuǎn)換)
有時修械,處理一系列可能是順序或并行趾牧,長或短,同步或異步以及可能導(dǎo)致各種結(jié)果的活動可能變得非常難以編程肯污。您可能還需要更新顯示以及進(jìn)度或根據(jù)狀態(tài)翘单。
第一個用例旨在使這種情況更容易處理。
該解決方案基于以下原則:
- 發(fā)出一個事件;
- 此事件觸發(fā)一些導(dǎo)致一個或多個狀態(tài)的動作;
- 這些狀態(tài)中的每一個都可以反過來發(fā)出其他事件或?qū)е铝硪粋€狀態(tài);
- 然后蹦渣,這些事件將根據(jù)活動狀態(tài)觸發(fā)其他操作;
- 等等…
為了說明這個概念哄芜,我們來看兩個常見的例子:
應(yīng)用初始化
- 假設(shè)您需要運行一系列操作來初始化應(yīng)用程序。操作可能與服務(wù)器的交互相關(guān)聯(lián)(例如柬唯,加載一些數(shù)據(jù))认臊。
在此初始化過程中,您可能需要顯示進(jìn)度條和一系列圖像以使用戶等待锄奢。
認(rèn)證
- 在啟動時失晴,應(yīng)用程序可能需要用戶進(jìn)行身份驗證或注冊。
用戶通過身份驗證后拘央,將重定向到應(yīng)用程序的主頁面涂屁。然后,如果用戶注銷灰伟,則將其重定向到認(rèn)證頁面拆又。
為了能夠處理所有可能的情況,事件序列,但是如果我們認(rèn)為可以在應(yīng)用程序中的任何地方觸發(fā)事件帖族,這可能變得非常難以管理义矛。
這就是BlocEventState,兼有BlocEventStateBuilder盟萨,可以幫助很多...
3.1凉翻。BlocEventState
BlocEventState背后的想法是定義一個BLoC:
- 接受事件作為輸入;
- 當(dāng)發(fā)出新事件時調(diào)用eventHandler;
- eventHandler 負(fù)責(zé)根據(jù)事件采取適當(dāng)?shù)男袆硬l(fā)出狀態(tài)作為回應(yīng)。
下圖顯示了這個想法:
這是這類的源代碼捻激。解釋如下:
import 'package:blocs/bloc_helpers/bloc_provider.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
abstract class BlocEvent extends Object {}
abstract class BlocState extends Object {}
abstract class BlocEventStateBase<BlocEvent, BlocState> implements BlocBase {
PublishSubject<BlocEvent> _eventController = PublishSubject<BlocEvent>();
BehaviorSubject<BlocState> _stateController = BehaviorSubject<BlocState>();
///
/// 要調(diào)用以發(fā)出事件
///
Function(BlocEvent) get emitEvent => _eventController.sink.add;
///
/// 當(dāng)前/新狀態(tài)
///
Stream<BlocState> get state => _stateController.stream;
///
/// 事件的外部處理
///
Stream<BlocState> eventHandler(BlocEvent event, BlocState currentState);
///
/// initialState
///
final BlocState initialState;
//
// 構(gòu)造函數(shù)
//
BlocEventStateBase({
@required this.initialState,
}){
//
// 對于每個接收到的事件制轰,我們調(diào)用[eventHandler]并發(fā)出任何結(jié)果的newState
//
_eventController.listen((BlocEvent event){
BlocState currentState = _stateController.value ?? initialState;
eventHandler(event, currentState).forEach((BlocState newState){
_stateController.sink.add(newState);
});
});
}
@override
void dispose() {
_eventController.close();
_stateController.close();
}
}
如您所見,這是一個需要擴展的抽象類胞谭,用于定義eventHandler方法的行為垃杖。
他公開:
- 一個Sink(emitEvent)來推送一個事件 ;
- 一個流(狀態(tài))來監(jiān)聽發(fā)射狀態(tài)。
在初始化時(請參閱構(gòu)造函數(shù)):
一個初始化狀態(tài)需要設(shè)置;
- 它創(chuàng)建了一個StreamSubscription聽傳入事件到
- 將它們發(fā)送到eventHandler
- 發(fā)出結(jié)果狀態(tài)丈屹。
3.2. 專門的BlocEventState
用于實現(xiàn)此類BlocEventState的模板在下面給出调俘。之后,我們將實施真實的旺垒。
class TemplateEventStateBloc extends BlocEventStateBase<BlocEvent, BlocState> {
TemplateEventStateBloc()
: super(
initialState: BlocState.notInitialized(),
);
@override
Stream<BlocState> eventHandler( BlocEvent event, BlocState currentState) async* {
yield BlocState.notInitialized();
}
}
如果這個模板不能編譯彩库,請不要擔(dān)心......這是正常的,因為我們還沒有定義BlocState.notInitialized() ......這將在幾分鐘內(nèi)出現(xiàn)先蒋。
此模板僅在初始化時提供initialState并覆蓋eventHandler骇钦。
這里有一些非常有趣的事情需要注意。我們使用異步生成器:async * 和yield語句竞漾。
使用async *修飾符標(biāo)記函數(shù)眯搭,將函數(shù)標(biāo)識為異步生成器:
每次 yield 語句 被調(diào)用時,它增加了下面的表達(dá)式的結(jié)果 yield 輸出stream业岁。
這是非常有用的鳞仙,如果我們需要發(fā)出一個序列的States,從一系列的行動所造成(我們將在后面看到笔时,在實踐中)
有關(guān)異步生成器的其他詳細(xì)信息棍好,請單擊此鏈接。
3.3.BlocEvent和BlocState
正如您所注意到的糊闽,我們已經(jīng)定義了一個 BlocEvent 和 BlocState 抽象類梳玫。
這些類需要使用您要發(fā)出的特殊事件和狀態(tài)進(jìn)行擴展。
3.4. BlocEventStateBuilder小部件
模式最后一部分的是BlocEventStateBuilder小部件右犹,它允許你在響應(yīng)State(s)提澎,所發(fā)射的BlocEventState。
這是它的源代碼:
typedef Widget AsyncBlocEventStateBuilder<BlocState>(BuildContext context, BlocState state);
class BlocEventStateBuilder<BlocEvent,BlocState> extends StatelessWidget {
const BlocEventStateBuilder({
Key key,
@required this.builder,
@required this.bloc,
}): assert(builder != null),
assert(bloc != null),
super(key: key);
final BlocEventStateBase<BlocEvent,BlocState> bloc;
final AsyncBlocEventStateBuilder<BlocState> builder;
@override
Widget build(BuildContext context){
return StreamBuilder<BlocState>(
stream: bloc.state,
initialData: bloc.initialState,
builder: (BuildContext context, AsyncSnapshot<BlocState> snapshot){
return builder(context, snapshot.data);
},
);
}
}
這個Widget只是一個專門的StreamBuilder念链,它會在每次發(fā)出新的BlocState時調(diào)用builder輸入?yún)?shù)盼忌。
好的』矗現(xiàn)在我們已經(jīng)擁有了所有的部分,現(xiàn)在是時候展示我們可以用它們做些什么......
3.5.案例1:應(yīng)用程序初始化
第一個示例說明了您需要應(yīng)用程序在啟動時執(zhí)行某些任務(wù)的情況谦纱。
常見的用途是游戲最初顯示啟動畫面(動畫與否)看成,同時從服務(wù)器獲取一些文件,檢查新的更新是否可用跨嘉,嘗試連接到任何游戲中心 ......在顯示實際主屏幕之前川慌。為了不給應(yīng)用程序什么都不做的感覺,它可能會顯示一個進(jìn)度條并定期顯示一些圖片祠乃,同時它會完成所有初始化過程梦重。
我要向您展示的實現(xiàn)非常簡單。它只會在屏幕上顯示一些競爭百分比亮瓷,但這可以很容易地擴展到您的需求琴拧。
3.5.1。ApplicationInitializationEvent
在這個例子中嘱支,我只考慮2個事件:
- start:此事件將觸發(fā)初始化過程;
- stop:該事件可用于強制初始化進(jìn)程停止蚓胸。
這是定義代碼實現(xiàn):
class ApplicationInitializationEvent extends BlocEvent {
final ApplicationInitializationEventType type;
ApplicationInitializationEvent({
this.type: ApplicationInitializationEventType.start,
}) : assert(type != null);
}
enum ApplicationInitializationEventType {
start,
stop,
}
3.5.2. ApplicationInitializationState
該類將提供與初始化過程相關(guān)的信息。
對于這個例子除师,我會考慮:
- 2標(biāo)識:
isInitialized指示初始化是否完成
isInitializing以了解我們是否處于初始化過程的中間- 進(jìn)度完成率
這是它的源代碼:
class ApplicationInitializationState extends BlocState {
ApplicationInitializationState({
@required this.isInitialized,
this.isInitializing: false,
this.progress: 0,
});
final bool isInitialized;
final bool isInitializing;
final int progress;
factory ApplicationInitializationState.notInitialized() {
return ApplicationInitializationState(
isInitialized: false,
);
}
factory ApplicationInitializationState.progressing(int progress) {
return ApplicationInitializationState(
isInitialized: progress == 100,
isInitializing: true,
progress: progress,
);
}
factory ApplicationInitializationState.initialized() {
return ApplicationInitializationState(
isInitialized: true,
progress: 100,
);
}
}
3.5.3. ApplicationInitializationBloc
該BLoC負(fù)責(zé)基于事件處理初始化過程沛膳。
這是代碼:
class ApplicationInitializationBloc
extends BlocEventStateBase<ApplicationInitializationEvent, ApplicationInitializationState> {
ApplicationInitializationBloc()
: super(
initialState: ApplicationInitializationState.notInitialized(),
);
@override
Stream<ApplicationInitializationState> eventHandler(
ApplicationInitializationEvent event, ApplicationInitializationState currentState) async* {
if (!currentState.isInitialized){
yield ApplicationInitializationState.notInitialized();
}
if (event.type == ApplicationInitializationEventType.start) {
for (int progress = 0; progress < 101; progress += 10){
await Future.delayed(const Duration(milliseconds: 300));
yield ApplicationInitializationState.progressing(progress);
}
}
if (event.type == ApplicationInitializationEventType.stop){
yield ApplicationInitializationState.initialized();
}
}
}
一些解釋:
- 當(dāng)收到事件“ ApplicationInitializationEventType.start ”時,它從0開始計數(shù)到100(單位為10)馍盟,并且對于每個值(0,10,20于置,......),它發(fā)出(通過yield)一個告訴的新狀態(tài)初始化正在運行(isInitializing = true)及其進(jìn)度值贞岭。
- 當(dāng)收到事件"ApplicationInitializationEventType.stop"時,它認(rèn)為初始化已完成搓侄。
- 正如你所看到的瞄桨,我在計數(shù)器循環(huán)中放了一些延遲。這將向您展示如何使用任何Future(例如讶踪,您需要聯(lián)系服務(wù)器的情況)
3.5.4. 將它們?nèi)堪b在一起
現(xiàn)在芯侥,剩下的部分是顯示顯示計數(shù)器的偽Splash屏幕 ......
class InitializationPage extends StatefulWidget {
@override
_InitializationPageState createState() => _InitializationPageState();
}
class _InitializationPageState extends State<InitializationPage> {
ApplicationInitializationBloc bloc;
@override
void initState(){
super.initState();
bloc = ApplicationInitializationBloc();
bloc.emitEvent(ApplicationInitializationEvent());
}
@override
void dispose(){
bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext pageContext) {
return SafeArea(
child: Scaffold(
body: Container(
child: Center(
child: BlocEventStateBuilder<ApplicationInitializationEvent, ApplicationInitializationState>(
bloc: bloc,
builder: (BuildContext context, ApplicationInitializationState state){
if (state.isInitialized){
//
// Once the initialization is complete, let's move to another page
//
WidgetsBinding.instance.addPostFrameCallback((_){
Navigator.of(context).pushReplacementNamed('/home');
});
}
return Text('Initialization in progress... ${state.progress}%');
},
),
),
),
),
);
}
}
說明:
- 由于ApplicationInitializationBloc不需要在應(yīng)用程序的任何地方使用,我們可以在StatefulWidget中初始化它;
- 我們直接發(fā)出ApplicationInitializationEventType.start事件來觸發(fā)eventHandler
- 每次發(fā)出ApplicationInitializationState時乳讥,我們都會更新文本
- 初始化完成后柱查,我們將用戶重定向到主頁。
特技
由于我們無法直接重定向到主頁云石,在構(gòu)建器內(nèi)部唉工,我們使用WidgetsBinding.instance.addPostFrameCallback()方法請求Flutter 在渲染完成后立即執(zhí)行方法
3.6. 案例2:應(yīng)用程序身份驗證和注銷
對于此示例,我將考慮以下用例:
- 在啟動時汹忠,如果用戶未經(jīng)過身份驗證淋硝,則會自動顯示“ 身份驗證/注冊”頁面;
- 在用戶認(rèn)證期間雹熬,顯示CircularProgressIndicator ;
- 經(jīng)過身份驗證后,用戶將被重定向到主頁 ;
- 在應(yīng)用程序的任何地方谣膳,用戶都可以注銷;
- 當(dāng)用戶注銷時竿报,用戶將自動重定向到“ 身份驗證”頁面。
當(dāng)然继谚,很有可能以編程方式處理所有這些烈菌,但將所有這些委托給BLoC要容易得多。
下圖解釋了我要解釋的解決方案:
名為“ DecisionPage ” 的中間頁面將負(fù)責(zé)將用戶自動重定向到“ 身份驗證”頁面或主頁花履,具體取決于用戶身份驗證的狀態(tài)芽世。當(dāng)然,此DecisionPage從不顯示臭挽,也不應(yīng)被視為頁面捂襟。
3.6.1. AuthenticationEvent
在這個例子中,我只考慮2個事件:
- login:當(dāng)用戶正確驗證時發(fā)出此事件;
- logout:用戶注銷時發(fā)出的事件欢峰。
代碼如下:
abstract class AuthenticationEvent extends BlocEvent {
final String name;
AuthenticationEvent({
this.name: '',
});
}
class AuthenticationEventLogin extends AuthenticationEvent {
AuthenticationEventLogin({
String name,
}) : super(
name: name,
);
}
class AuthenticationEventLogout extends AuthenticationEvent {}
3.6.2. AuthenticationState
該類將提供與身份驗證過程相關(guān)的信息葬荷。
對于這個例子,我會考慮:
- 3點:
isAuthenticated指示身份驗證是否完整
isAuthenticating以了解我們是否處于身份驗證過程的中間
hasFailed表示身份驗證失敗- 經(jīng)過身份驗證的用戶名
這是它的源代碼:
class AuthenticationState extends BlocState {
AuthenticationState({
@required this.isAuthenticated,
this.isAuthenticating: false,
this.hasFailed: false,
this.name: '',
});
final bool isAuthenticated;
final bool isAuthenticating;
final bool hasFailed;
final String name;
factory AuthenticationState.notAuthenticated() {
return AuthenticationState(
isAuthenticated: false,
);
}
factory AuthenticationState.authenticated(String name) {
return AuthenticationState(
isAuthenticated: true,
name: name,
);
}
factory AuthenticationState.authenticating() {
return AuthenticationState(
isAuthenticated: false,
isAuthenticating: true,
);
}
factory AuthenticationState.failure() {
return AuthenticationState(
isAuthenticated: false,
hasFailed: true,
);
}
}
3.6.3.AuthenticationBloc
此BLoC負(fù)責(zé)根據(jù)事件處理身份驗證過程纽帖。
這是代碼:
class AuthenticationBloc
extends BlocEventStateBase<AuthenticationEvent, AuthenticationState> {
AuthenticationBloc()
: super(
initialState: AuthenticationState.notAuthenticated(),
);
@override
Stream<AuthenticationState> eventHandler(
AuthenticationEvent event, AuthenticationState currentState) async* {
if (event is AuthenticationEventLogin) {
//通知我們正在進(jìn)行身份驗證
yield AuthenticationState.authenticating();
//模擬對身份驗證服務(wù)器的調(diào)用
await Future.delayed(const Duration(seconds: 2));
//告知我們是否已成功通過身份驗證
if (event.name == "failure"){
yield AuthenticationState.failure();
} else {
yield AuthenticationState.authenticated(event.name);
}
}
if (event is AuthenticationEventLogout){
yield AuthenticationState.notAuthenticated();
}
}
}
一些解釋:
- 當(dāng)收到事件“ AuthenticationEventLogin ”時宠漩,它會(通過yield)發(fā)出一個新狀態(tài),告知身份驗證正在運行(isAuthenticating = true)懊直。
- 然后它運行身份驗證扒吁,一旦完成,就會發(fā)出另一個狀態(tài)室囊,告知身份驗證已完成雕崩。
- 當(dāng)收到事件“ AuthenticationEventLogout ”時,它將發(fā)出一個新狀態(tài)融撞,告訴用戶不再進(jìn)行身份驗證盼铁。
3.6.4. AuthenticationPage
正如您將要看到的那樣,為了便于解釋尝偎,此頁面非橙幕穑基本且不會做太多。
這是代碼致扯。解釋如下:
class AuthenticationPage extends StatelessWidget {
///
/// Prevents the use of the "back" button
///
Future<bool> _onWillPopScope() async {
return false;
}
@override
Widget build(BuildContext context) {
AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
return WillPopScope(
onWillPop: _onWillPopScope,
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Authentication Page'),
leading: Container(),
),
body: Container(
child:
BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
bloc: bloc,
builder: (BuildContext context, AuthenticationState state) {
if (state.isAuthenticating) {
return PendingAction();
}
if (state.isAuthenticated){
return Container();
}
List<Widget> children = <Widget>[];
// Button to fake the authentication (success)
children.add(
ListTile(
title: RaisedButton(
child: Text('Log in (success)'),
onPressed: () {
bloc.emitEvent(AuthenticationEventLogin(name: 'Didier'));
},
),
),
);
// Button to fake the authentication (failure)
children.add(
ListTile(
title: RaisedButton(
child: Text('Log in (failure)'),
onPressed: () {
bloc.emitEvent(AuthenticationEventLogin(name: 'failure'));
},
),
),
);
// Display a text if the authentication failed
if (state.hasFailed){
children.add(
Text('Authentication failure!'),
);
}
return Column(
children: children,
);
},
),
),
),
),
);
}
}
說明:
- 第11行:頁面檢索對AuthenticationBloc的引用
- 第24-70行:它監(jiān)聽發(fā)出的AuthenticationState:
如果身份驗證正在進(jìn)行中肤寝,它會顯示一個CircularProgressIndicator,告訴用戶正在進(jìn)行某些操作并阻止用戶訪問該頁面(第25-27行)
如果驗證成功抖僵,我們不需要顯示任何內(nèi)容(第29-31行)鲤看。
如果用戶未經(jīng)過身份驗證,則會顯示2個按鈕以模擬成功的身份驗證和失敗裆针。
當(dāng)我們點擊其中一個按鈕時刨摩,我們發(fā)出一個AuthenticationEventLogin事件寺晌,以及一些參數(shù)(通常由認(rèn)證過程使用)
如果驗證失敗,我們會顯示錯誤消息(第60-64行)
提示
您可能已經(jīng)注意到澡刹,我將頁面包裝在WillPopScope中呻征。
理由是我不希望用戶能夠使用Android'后退'按鈕,如此示例中所示罢浇,身份驗證是一個必須的步驟陆赋,它阻止用戶訪問任何其他部分,除非經(jīng)過正確的身份驗證嚷闭。
3.6.5. DecisionPage
如前所述攒岛,我希望應(yīng)用程序根據(jù)身份驗證狀態(tài)自動重定向到AuthenticationPage或HomePage。
以下是此DecisionPage的代碼胞锰,說明如下:
class DecisionPage extends StatefulWidget {
@override
DecisionPageState createState() {
return new DecisionPageState();
}
}
class DecisionPageState extends State<DecisionPage> {
AuthenticationState oldAuthenticationState;
@override
Widget build(BuildContext context) {
AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
return BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
bloc: bloc,
builder: (BuildContext context, AuthenticationState state) {
if (state != oldAuthenticationState){
oldAuthenticationState = state;
if (state.isAuthenticated){
_redirectToPage(context, HomePage());
} else if (state.isAuthenticating || state.hasFailed){
//do nothing
} else {
_redirectToPage(context, AuthenticationPage());
}
}//此頁面不需要顯示任何內(nèi)容
//總是在任何活動頁面后面提醒(因此“隱藏”)灾锯。
return Container();
}
);
}
void _redirectToPage(BuildContext context, Widget page){
WidgetsBinding.instance.addPostFrameCallback((_){
MaterialPageRoute newRoute = MaterialPageRoute(
builder: (BuildContext context) => page
);
Navigator.of(context).pushAndRemoveUntil(newRoute, ModalRoute.withName('/decision'));
});
}
}
提醒
為了詳細(xì)解釋這一點,我們需要回到Flutter處理Pages(= Route)的方式嗅榕。要處理路由顺饮,我們使用導(dǎo)航器,它創(chuàng)建一個疊加層凌那。
這個覆蓋是一個堆棧的OverlayEntry兼雄,他們每個人的包含頁面。
當(dāng)我們通過Navigator.of(上下文)推送帽蝶,彈出赦肋,替換頁面時,后者更新其重建的覆蓋(因此堆棧)励稳。
當(dāng)堆棧被重建佃乘,每個OverlayEntry(因此它的內(nèi)容)也被重建。
因此驹尼,當(dāng)我們通過Navigator.of(上下文)進(jìn)行操作時恕稠,所有剩余的頁面都會重建!
那么扶欣,為什么我將它實現(xiàn)為StatefulWidget?
為了能夠響應(yīng)AuthenticationState的任何更改千扶,此“ 頁面 ”需要在應(yīng)用程序的整個生命周期中保持存在料祠。
這意味著,根據(jù)上面的提醒澎羞,每次Navigator.of(上下文)完成操作時髓绽,都會重建此頁面。
因此妆绞,它的BlocEventStateBuilder也將重建顺呕,調(diào)用自己的構(gòu)建器方法枫攀。
因為此構(gòu)建器負(fù)責(zé)將用戶重定向到與AuthenticationState對應(yīng)的頁面,所以如果我們每次重建頁面時重定向用戶株茶,它將繼續(xù)重定向来涨,因為不斷重建。
為了防止這種情況發(fā)生启盛,我們只需要記住我們采取行動的最后一個AuthenticationState蹦掐,并且只在收到另一個AuthenticationState時采取另一個動作。
這是如何運作的僵闯?
如上所述卧抗,每次發(fā)出AuthenticationState時,BlocEventStateBuilder都會調(diào)用其構(gòu)建器鳖粟。
基于狀態(tài)標(biāo)志(isAuthenticated)社裆,我們知道我們需要向哪個頁面重定向用戶。
特技
由于我們無法直接從構(gòu)建器重定向到另一個頁面向图,因此我們使用WidgetsBinding.instance.addPostFrameCallback()方法在呈現(xiàn)完成后請求Flutter執(zhí)行方法
此外泳秀,由于我們需要在重定向用戶之前刪除任何現(xiàn)有頁面,除了需要保留在所有情況下的此DecisionPage 之外张漂,我們使用Navigator.of(context).pushAndRemoveUntil(...)來實現(xiàn)此目的晶默。
3.6.6、登出
要讓用戶注銷航攒,您現(xiàn)在可以創(chuàng)建一個“ LogOutButton ”并將其放在應(yīng)用程序的任何位置磺陡。
- 此按鈕只需要發(fā)出AuthenticationEventLogout()事件,這將導(dǎo)致以下自動操作鏈:
1.它將由AuthenticationBloc處理
2.反過來會發(fā)出一個AuthentiationState(isAuthenticated = false)
3.這將由DecisionPage通過BlocEventStateBuilder處理
4.這會將用戶重定向到AuthenticationPage
3.6.7. AuthenticationBloc
由于AuthenticationBloc需要提供給該應(yīng)用程序的任何頁面漠畜,我們也將注入它作為MaterialApp父母币他,如下所示
void main() => runApp(Application());
class Application extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<AuthenticationBloc>(
bloc: AuthenticationBloc(),
child: MaterialApp(
title: 'BLoC Samples',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: DecisionPage(),
),
);
}
}
4.表格驗證(允許根據(jù)條目和驗證控制表單的行為)
BLoC的另一個有趣用途是當(dāng)您需要驗證表單時:
- 根據(jù)某些業(yè)務(wù)規(guī)則驗證與TextField相關(guān)的條目;
- 根據(jù)規(guī)則顯示驗證錯誤消息;
- 根據(jù)業(yè)務(wù)規(guī)則自動化窗口小部件的可訪問性。
我現(xiàn)在要做的一個例子是RegistrationForm憔狞,它由3個TextFields(電子郵件蝴悉,密碼,確認(rèn)密碼)和1個RaisedButton組成瘾敢,以啟動注冊過程拍冠。
我想要實現(xiàn)的業(yè)務(wù)規(guī)則是:
- 該電子郵件必須是一個有效的電子郵件地址。如果不是簇抵,則需要顯示消息庆杜。
- 該密碼必須是有效的(必須包含至少8個字符,具有1個大寫碟摆,小寫1晃财,圖1和1個特殊字符)。如果無效典蜕,則需要顯示消息断盛。
- 在重新輸入密碼需要滿足相同的驗證規(guī)則和相同的密碼罗洗。如果不相同,則需要顯示消息钢猛。
- 在登記時伙菜,按鈕可能只能激活所有的規(guī)則都是有效的。
4.1.RegistrationFormBloc
該BLoC負(fù)責(zé)處理驗證業(yè)務(wù)規(guī)則厢洞,如前所述仇让。
源碼如下:
class RegistrationFormBloc extends Object with EmailValidator, PasswordValidator implements BlocBase {
final BehaviorSubject<String> _emailController = BehaviorSubject<String>();
final BehaviorSubject<String> _passwordController = BehaviorSubject<String>();
final BehaviorSubject<String> _passwordConfirmController = BehaviorSubject<String>();
//
// Inputs
//
Function(String) get onEmailChanged => _emailController.sink.add;
Function(String) get onPasswordChanged => _passwordController.sink.add;
Function(String) get onRetypePasswordChanged => _passwordConfirmController.sink.add;
//
// Validators
//
Stream<String> get email => _emailController.stream.transform(validateEmail);
Stream<String> get password => _passwordController.stream.transform(validatePassword);
Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
.doOnData((String c){
// If the password is accepted (after validation of the rules)
// we need to ensure both password and retyped password match
if (0 != _passwordController.value.compareTo(c)){
// If they do not match, add an error
_passwordConfirmController.addError("No Match");
}
});
//
// Registration button
Stream<bool> get registerValid => Observable.combineLatest3(
email,
password,
confirmPassword,
(e, p, c) => true
);
@override
void dispose() {
_emailController?.close();
_passwordController?.close();
_passwordConfirmController?.close();
}
}
讓我詳細(xì)解釋一下......
- 我們首先初始化3個BehaviorSubject來處理表單的每個TextField的Streams。
- 我們公開了3個Function(String)躺翻,它將用于接受來自TextFields的輸入丧叽。
- 我們公開了3個Stream <String>,TextField將使用它來顯示由它們各自的驗證產(chǎn)生的潛在錯誤消息
- 我們公開了1個Stream <bool>公你,它將被RaisedButton使用踊淳,以根據(jù)整個驗證結(jié)果啟用/禁用它。
好的陕靠,現(xiàn)在是時候深入了解更多細(xì)節(jié)......
您可能已經(jīng)注意到迂尝,此類的簽名有點特殊。我們來回顧一下吧剪芥。
class RegistrationFormBloc extends Object
with EmailValidator, PasswordValidator
implements BlocBase {
...
}
with 關(guān)鍵字意味著這個類是使用混入(MIXINS)(在另一個類中重用一些類代碼的一種方法)垄开,為了能夠使用with關(guān)鍵字,該類需要擴展Object類税肪。這些mixin包含分別驗證電子郵件和密碼的代碼溉躲。
有關(guān)詳細(xì)信息,混入我建議你閱讀從這篇大文章 Romain Rastel益兄。
4.1.1. Validator Mixins
我只會解釋EmailValidator锻梳,因為PasswordValidator非常相似。
First, the code:
const String _kEmailRule = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";
class EmailValidator {
final StreamTransformer<String,String> validateEmail =
StreamTransformer<String,String>.fromHandlers(handleData: (email, sink){
final RegExp emailExp = new RegExp(_kEmailRule);
if (!emailExp.hasMatch(email) || email.isEmpty){
sink.addError('Entre a valid email');
} else {
sink.add(email);
}
});
}
該類公開了一個 final 函數(shù)(“ validateEmail ”)净捅,它是一個StreamTransformer疑枯。
提醒
StreamTransformer被調(diào)用如下:stream.transform(StreamTransformer)儡嘶。
StreamTransformer通過transform方法從Stream引用它的輸入嘹裂。然后它處理此輸入寡痰,并將轉(zhuǎn)換后的輸入重新注入初始Stream赏半。
4.1.2. 為什么使用stream.transform()?
如前所述货邓,如果驗證成功怎茫,StreamTransformer會將輸入重新注入Stream诊杆。為什么有用捉腥?
以下是與Observable.combineLatest3()相關(guān)的解釋...此方法在它引用的所有Streams之前不會發(fā)出任何值,至少發(fā)出一個值你画。
讓我們看看下面的圖片來說明我們想要實現(xiàn)的目標(biāo)抵碟。
如果用戶輸入電子郵件并且后者經(jīng)過驗證桃漾,它將由電子郵件流發(fā)出,該電子郵件流將是Observable.combineLatest3()的一個輸入;
如果電子郵件地址無效拟逮,錯誤將被添加到流(和沒有價值會流出流);
這同樣適用于密碼和重新輸入密碼 ;
當(dāng)所有這三個驗證都成功時(意味著所有這三個流都會發(fā)出一個值)撬统,Observable.combineLatest3()將依次發(fā)出一個真正的感謝“ (e,p敦迄,c)=> true ”(見第35行)恋追。
4.1.3. 驗證2個密碼
我在互聯(lián)網(wǎng)上看到了很多與這種比較有關(guān)的問題。存在幾種解決方案罚屋,讓我解釋其中的兩種苦囱。
4.1.3.1.基本解決方案 - 沒有錯誤消息
第一個解決方案可能是以下一個:
Stream<bool> get registerValid => Observable.combineLatest3(
email,
password,
confirmPassword,
(e, p, c) => (0 == p.compareTo(c))
);
這個解決方案只需驗證兩個密碼,如果它們匹配脾猛,就會發(fā)出一個值(= true)撕彤。
我們很快就會看到,Register按鈕的可訪問性將取決于registerValid流猛拴。
如果兩個密碼不匹配羹铅,則該流不會發(fā)出任何值,并且“ 注冊”按鈕保持不活動狀態(tài)愉昆,但用戶不會收到任何錯誤消息以幫助他理解原因职员。
4.1.3.2。帶錯誤消息的解決方案
另一種解決方案包括擴展confirmPassword流的處理跛溉,如下所示:
Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
.doOnData((String c){
//如果接受密碼(在驗證規(guī)則后)
//我們需要確保密碼和重新輸入的密碼匹配
if (0 != _passwordController.value.compareTo(c)){
//如果它們不匹配焊切,請?zhí)砑渝e誤
_passwordConfirmController.addError("No Match");
}
});
一旦驗證了重新輸入密碼,它就會被Stream發(fā)出倒谷,并且使用doOnData蛛蒙,我們可以直接獲取此發(fā)出的值并將其與密碼流的值進(jìn)行比較。如果兩者不匹配渤愁,我們現(xiàn)在可以發(fā)送錯誤消息牵祟。
4.2. The RegistrationForm
現(xiàn)在讓我們先解釋一下RegistrationForm:
class RegistrationForm extends StatefulWidget {
@override
_RegistrationFormState createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
RegistrationFormBloc _registrationFormBloc;
@override
void initState() {
super.initState();
_registrationFormBloc = RegistrationFormBloc();
}
@override
void dispose() {
_registrationFormBloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
StreamBuilder<String>(
stream: _registrationFormBloc.email,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'email',
errorText: snapshot.error,
),
onChanged: _registrationFormBloc.onEmailChanged,
keyboardType: TextInputType.emailAddress,
);
}),
StreamBuilder<String>(
stream: _registrationFormBloc.password,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'password',
errorText: snapshot.error,
),
obscureText: false,
onChanged: _registrationFormBloc.onPasswordChanged,
);
}),
StreamBuilder<String>(
stream: _registrationFormBloc.confirmPassword,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'retype password',
errorText: snapshot.error,
),
obscureText: false,
onChanged: _registrationFormBloc.onRetypePasswordChanged,
);
}),
StreamBuilder<bool>(
stream: _registrationFormBloc.registerValid,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return RaisedButton(
child: Text('Register'),
onPressed: (snapshot.hasData && snapshot.data == true)
? () {
// launch the registration process
}
: null,
);
}),
],
),
);
}
}
說明:
- 由于RegisterFormBloc僅供此表單使用,因此適合在此處初始化它抖格。
- 每個TextField都包裝在StreamBuilder <String>中诺苹,以便能夠響應(yīng)驗證過程的任何結(jié)果(請參閱errorText:snapshot.error)
- 每次對TextField的內(nèi)容進(jìn)行修改時,我們都會通過onChanged發(fā)送輸入到BLoC進(jìn)行驗證:_registrationFormBloc.onEmailChanged(電子郵件輸入的情況)
- 對于RegisterButton雹拄,后者也包含在StreamBuilder <bool>中收奔。
- 如果_registrationFormBloc.registerValid發(fā)出一個值,onPressed方法將執(zhí)行某些操作
- 如果未發(fā)出任何值滓玖,則onPressed方法將被指定為null坪哄,這將取消激活該按鈕。
而已!表單中沒有任何業(yè)務(wù)規(guī)則翩肌,這意味著可以更改規(guī)則而無需對表單進(jìn)行任何修改模暗,這非常好!
5.Part Of(允許Widget根據(jù)其在列表中的存在來調(diào)整其行為)
有時念祭,Widget知道它是否是驅(qū)動其行為的集合的一部分是有趣的兑宇。
對于本文的最后一個用例,我將考慮以下場景:
應(yīng)用程序處理項目;
用戶可以選擇放入購物籃的物品;
一件商品只能放入購物籃一次;
存放在購物籃中的物品可以從購物籃中取出;
一旦被移除粱坤,就可以將其取回隶糕。
對于此示例,每個項目將顯示一個按鈕站玄,該按鈕將取決于購物籃中物品的存在枚驻。如果不是購物籃的一部分,該按鈕將允許用戶將其添加到購物籃中蜒什。如果是購物籃的一部分测秸,該按鈕將允許用戶將其從籃子中取出。
為了更好地說明“ 部分 ”模式灾常,我將考慮以下架構(gòu):
一個購物頁面將顯示所有可能的項目清單;
購物頁面中的每個商品都會顯示一個按鈕霎冯,用于將商品添加到購物籃或?qū)⑵湟瞥唧w取決于其在購物籃中的位置;
如果一個項目在購物頁被添加到籃钞瀑,它的按鈕將自動更新沈撞,以允許用戶從所述籃(反之亦然)將其刪除,而不必重新生成購物頁
另一頁雕什,購物籃缠俺,將列出籃子里的所有物品;
可以從此頁面中刪除購物籃中的任何商品。
邊注
Part Of這個名字是我給的個人名字贷岸。這不是官方名稱壹士。
正如您現(xiàn)在可以想象的那樣,我們需要考慮一個專門用于處理所有可能項目列表的BLoC偿警,以及購物籃的一部分躏救。
這個BLoC可能如下所示:
class ShoppingBloc implements BlocBase {
// 所有商品的清單,購物籃的一部分
Set<ShoppingItem> _shoppingBasket = Set<ShoppingItem>();
// 流到所有可能項目的列表
BehaviorSubject<List<ShoppingItem>> _itemsController = BehaviorSubject<List<ShoppingItem>>();
Stream<List<ShoppingItem>> get items => _itemsController;
// Stream以列出購物籃中的項目部分
BehaviorSubject<List<ShoppingItem>> _shoppingBasketController = BehaviorSubject<List<ShoppingItem>>(seedValue: <ShoppingItem>[]);
Stream<List<ShoppingItem>> get shoppingBasket => _shoppingBasketController;
@override
void dispose() {
_itemsController?.close();
_shoppingBasketController?.close();
}
//構(gòu)造函數(shù)
ShoppingBloc() {
_loadShoppingItems();
}
void addToShoppingBasket(ShoppingItem item){
_shoppingBasket.add(item);
_postActionOnBasket();
}
void removeFromShoppingBasket(ShoppingItem item){
_shoppingBasket.remove(item);
_postActionOnBasket();
}
void _postActionOnBasket(){
// 使用新內(nèi)容提供購物籃流
_shoppingBasketController.sink.add(_shoppingBasket.toList());
// 任何其他處理螟蒸,如
// 計算籃子的總價
// 項目數(shù)量盒使,籃子的一部分......
}
//
//生成一系列購物項目
//通常這應(yīng)該來自對服務(wù)器的調(diào)用
//但是對于這個樣本,我們只是模擬
//
void _loadShoppingItems() {
_itemsController.sink.add(List<ShoppingItem>.generate(50, (int index) {
return ShoppingItem(
id: index,
title: "Item $index",
price: ((Random().nextDouble() * 40.0 + 10.0) * 100.0).roundToDouble() /
100.0,
color: Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0)
.withOpacity(1.0),
);
}));
}
}
唯一可能需要解釋的方法是_postActionOnBasket()方法七嫌。每次在籃子中添加或刪除項目時少办,我們都需要“刷新” _shoppingBasketController Stream 的內(nèi)容,以便通知所有正在監(jiān)聽此Stream更改的Widgets并能夠刷新/重建诵原。
5.2. ShoppingPage
此頁面非常簡單英妓,只顯示所有項目挽放。
class ShoppingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
ShoppingBloc bloc = BlocProvider.of<ShoppingBloc>(context);
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Shopping Page'),
actions: <Widget>[
ShoppingBasket(),
],
),
body: Container(
child: StreamBuilder<List<ShoppingItem>>(
stream: bloc.items,
builder: (BuildContext context,
AsyncSnapshot<List<ShoppingItem>> snapshot) {
if (!snapshot.hasData) {
return Container();
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ShoppingItemWidget(
shoppingItem: snapshot.data[index],
);
},
);
},
),
),
));
}
}
說明:
- 所述AppBar顯示按鈕,:
顯示出現(xiàn)在購物籃中的商品數(shù)量
單擊時將用戶重定向到ShoppingBasket頁面- 項目列表使用GridView構(gòu)建鞋拟,包含在StreamBuilder <List <ShoppingItem >>中
- 每個項目對應(yīng)一個ShoppingItemWidget
5.3.ShoppingBasketPage
此頁面與ShoppingPage非常相似骂维,只是StreamBuilder現(xiàn)在正在偵聽由ShoppingBloc公開的_shoppingBasket流的變體。
5.4. ShoppingItemWidget和ShoppingItemBloc
Part Of 模式依賴于這兩個元素的組合
- 該ShoppingItemWidget負(fù)責(zé):
顯示項目和
用于在購物籃中添加項目或從中取出項目的按鈕- 該ShoppingItemBloc負(fù)責(zé)告訴ShoppingItemWidget后者是否是購物籃的一部分贺纲,或者不是。
讓我們看看他們?nèi)绾我黄鸸ぷ?.....
5.4.1. ShoppingItemBloc
ShoppingItemBloc由每個ShoppingItemWidget實例化褪测,賦予它“身份”
此BLoC偵聽ShoppingBasket流的所有變體猴誊,并檢查特定項目標(biāo)識是否是籃子的一部分。
如果是侮措,它會發(fā)出一個布爾值(= true)懈叹,它將被ShoppingItemWidget捕獲,以確定它是否是籃子的一部分分扎。
這是BLoC的代碼:
class ShoppingItemBloc implements BlocBase {
// Stream澄成,如果ShoppingItemWidget是購物籃的一部分,則通知
BehaviorSubject<bool> _isInShoppingBasketController = BehaviorSubject<bool>();
Stream<bool> get isInShoppingBasket => _isInShoppingBasketController;
//收到所有商品列表的流畏吓,購物籃的一部分
PublishSubject<List<ShoppingItem>> _shoppingBasketController = PublishSubject<List<ShoppingItem>>();
Function(List<ShoppingItem>) get shoppingBasket => _shoppingBasketController.sink.add;
//具有shoppingItem的“標(biāo)識”的構(gòu)造方法
ShoppingItemBloc(ShoppingItem shoppingItem){
//每次購物籃內(nèi)容的變化
_shoppingBasketController.stream
//我們檢查這個shoppingItem是否是購物籃的一部分
.map((list) => list.any((ShoppingItem item) => item.id == shoppingItem.id))
// if it is part
.listen((isInShoppingBasket)
// we notify the ShoppingItemWidget
=> _isInShoppingBasketController.add(isInShoppingBasket));
}
@override
void dispose() {
_isInShoppingBasketController?.close();
_shoppingBasketController?.close();
}
}
5.4.2墨状。ShoppingItemWidget
此Widget負(fù)責(zé):
- 創(chuàng)建ShoppingItemBloc的實例并將其自己的標(biāo)識傳遞給BLoC
- 監(jiān)聽ShoppingBasket內(nèi)容的任何變化并將其轉(zhuǎn)移到BLoC
- 監(jiān)聽ShoppingItemBloc知道它是否是籃子的一部分
- 顯示相應(yīng)的按鈕(添加/刪除),具體取決于它在籃子中的存在
- 響應(yīng)按鈕的用戶操作
當(dāng)用戶點擊添加按鈕時菲饼,將自己添加到購物籃中
當(dāng)用戶點擊刪除按鈕時肾砂,將自己從籃子中移除。
讓我們看看它是如何工作的(解釋在代碼中給出)宏悦。
class ShoppingItemWidget extends StatefulWidget {
ShoppingItemWidget({
Key key,
@required this.shoppingItem,
}) : super(key: key);
final ShoppingItem shoppingItem;
@override
_ShoppingItemWidgetState createState() => _ShoppingItemWidgetState();
}
class _ShoppingItemWidgetState extends State<ShoppingItemWidget> {
StreamSubscription _subscription;
ShoppingItemBloc _bloc;
ShoppingBloc _shoppingBloc;
@override
void didChangeDependencies() {
super.didChangeDependencies();
//由于不應(yīng)在“initState()”方法中使用上下文镐确,
//在需要時更喜歡使用“didChangeDependencies()”
//在初始化時引用上下文
_initBloc();
}
@override
void didUpdateWidget(ShoppingItemWidget oldWidget) {
super.didUpdateWidget(oldWidget);
//因為Flutter可能決定重新組織Widgets樹
//最好重新創(chuàng)建鏈接
_disposeBloc();
_initBloc();
}
@override
void dispose() {
_disposeBloc();
super.dispose();
}
//這個例程對于創(chuàng)建鏈接是可靠的
void _initBloc() {
//創(chuàng)建ShoppingItemBloc的實例
_bloc = ShoppingItemBloc(widget.shoppingItem);
//檢索處理購物籃內(nèi)容的BLoC
_shoppingBloc = BlocProvider.of<ShoppingBloc>(context);
//傳輸購物內(nèi)容的簡單管道
//購物籃子到ShoppingItemBloc
_subscription = _shoppingBloc.shoppingBasket.listen(_bloc.shoppingBasket);
}
void _disposeBloc() {
_subscription?.cancel();
_bloc?.dispose();
}
Widget _buildButton() {
return StreamBuilder<bool>(
stream: _bloc.isInShoppingBasket,
initialData: false,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return snapshot.data
? _buildRemoveFromShoppingBasket()
: _buildAddToShoppingBasket();
},
);
}
Widget _buildAddToShoppingBasket(){
return RaisedButton(
child: Text('Add...'),
onPressed: (){
_shoppingBloc.addToShoppingBasket(widget.shoppingItem);
},
);
}
Widget _buildRemoveFromShoppingBasket(){
return RaisedButton(
child: Text('Remove...'),
onPressed: (){
_shoppingBloc.removeFromShoppingBasket(widget.shoppingItem);
},
);
}
@override
Widget build(BuildContext context) {
return Card(
child: GridTile(
header: Center(
child: Text(widget.shoppingItem.title),
),
footer: Center(
child: Text('${widget.shoppingItem.price} €'),
),
child: Container(
color: widget.shoppingItem.color,
child: Center(
child: _buildButton(),
),
),
),
);
}
}
5.5. 這一切如何運作?
下圖顯示了所有部分如何協(xié)同工作饼煞。
結(jié)論
另一篇長篇文章源葫,我希望我能縮短一點,但我認(rèn)為值得一些解釋砖瞧。
正如我在介紹中所說息堂,我個人在我的開發(fā)中經(jīng)常使用這些“ 模式 ”。這讓我節(jié)省了大量的時間和精力; 我的代碼更易讀芭届,更容易調(diào)試储矩。
此外,它有助于將業(yè)務(wù)與視圖分離褂乍。
大多數(shù)肯定有其他方法可以做到這一點持隧,甚至更好的方式,但它只對我有用逃片,這就是我想與你分享的一切屡拨。
請繼續(xù)關(guān)注新文章只酥,同時祝您編程愉快。