原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier Boelens 為 Reactive Programming - Streams - BLoC 寫的后續(xù)
閱讀本文前建議先閱讀前篇,前篇中文翻譯有兩個(gè)版本:
[譯]Flutter響應(yīng)式編程:Streams和BLoC by JarvanMo
忠于原作的版本Flutter中如何利用StreamBuilder和BLoC來控制Widget狀態(tài) by 吉原拉面
省略了一些初級(jí)概念衬浑,補(bǔ)充了一些個(gè)人解讀
前言
在了解 BLoC, Reactive Programming 和 Streams 概念后逆甜,我又花了些時(shí)間繼續(xù)研究舟肉,現(xiàn)在非常高興能夠與你們分享一些我經(jīng)常使用并且個(gè)人覺得很有用的模式(至少我是這么認(rèn)為的)谴餐。這些模式為我節(jié)約了大量的開發(fā)時(shí)間,并且讓代碼更加易讀和調(diào)試。
目錄
(由于原文較長橘洞,翻譯發(fā)布時(shí)進(jìn)行了分割)
BlocProvider 性能優(yōu)化
結(jié)合 StatefulWidget 和 InheritedWidget 兩者優(yōu)勢構(gòu)建 BlocProviderBLoC 的范圍和初始化
根據(jù) BLoC 的使用范圍初始化 BLoC事件與狀態(tài)管理
基于事件(Event) 的狀態(tài) (State) 變更響應(yīng)表單驗(yàn)證
根據(jù)表單項(xiàng)驗(yàn)證來控制表單行為 (范例中包含了表單中常用的密碼和重復(fù)密碼比對(duì))Part Of 模式
允許組件根據(jù)所處環(huán)境(是否在某個(gè)列表/集合/組件中)調(diào)整自身的行為
文中涉及的完整代碼可在 GitHub 查看。
4. 表單驗(yàn)證
BLoC 另一個(gè)有意思的應(yīng)用場景就是表單的驗(yàn)證说搅,比如:
- 驗(yàn)證某個(gè) TextField 表單項(xiàng)是否滿足一些業(yè)務(wù)規(guī)則
- 業(yè)務(wù)規(guī)則驗(yàn)證錯(cuò)誤時(shí)顯示提示信息
- 根據(jù)業(yè)務(wù)規(guī)則自動(dòng)處理表單組件是否可用
下面的例子中炸枣,我用了一個(gè)名叫 RegistrationForm 的表單,這個(gè)表單包含3個(gè) TextField (分別為電子郵箱email弄唧、密碼password和重復(fù)密碼 confirmPassword)以及一個(gè)按鈕 RaisedButton 用來發(fā)起注冊(cè)處理
想要實(shí)現(xiàn)的業(yè)務(wù)規(guī)則有:
- email 需要是有效的電子郵箱地址适肠,如果不是的話顯示錯(cuò)誤提示信息
- password 也必須需有效,即包括至少1個(gè)大寫字母候引、1個(gè)小寫字母侯养、1個(gè)數(shù)字和1個(gè)特殊字符在內(nèi),且不少于8位字符澄干,如果不是的話也需要顯示錯(cuò)誤提示信息
- 重復(fù)密碼 retype password 除了需要和 password 一樣的驗(yàn)證規(guī)則外逛揩,還需要和 password 完全一樣,如果不是的話麸俘,顯示錯(cuò)誤提示信息
- register 按鈕只有在以上所有規(guī)則都驗(yàn)證通過后才能使用
4.1. RegistrationFormBloc
如前所述辩稽,這個(gè) BLoC 負(fù)責(zé)業(yè)務(wù)規(guī)則驗(yàn)證的處理,實(shí)現(xiàn)的代碼如下:
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();
}
}
說明:
- 我們最先初始化了 3 個(gè) BehaviorSubject从媚,用來處理表單中 3 個(gè) TextField 的 Stream
- 提供了 3 個(gè) Function(String) 逞泄,用來接收來自 TextField 的輸入
- 提供了 3 個(gè) Stream<String> ,在 TextField 驗(yàn)證失敗時(shí)静檬,顯示各自的錯(cuò)誤信息
- 同時(shí)還提供了 1 個(gè) Stream<bool>炭懊,作用是根據(jù)全部表單項(xiàng)的驗(yàn)證結(jié)果,控制 RaisedButton 是否可用(enable/disabe)
好了拂檩,我們來深入了解更多的細(xì)節(jié)…
你可能注意到了侮腹,這個(gè) BLoC 類的代碼有點(diǎn)特殊,是這樣的:
class RegistrationFormBloc extends Object
with EmailValidator, PasswordValidator
implements BlocBase {
...
}
使用了 with 關(guān)鍵字表明這個(gè)類用到了 MIXINS (一種在另一個(gè)類中重用類代碼的方法)稻励,而且為了使用 with父阻,這個(gè)類還需要基于 Object 類進(jìn)行擴(kuò)展。這些 mixins 包含了 email 和 password 各自的驗(yàn)證方式望抽。
關(guān)于 Mixins 更多信息建議閱讀 Romain Rastel 的這篇文章加矛。
4.1.1. 表單驗(yàn)證用到的 Mixins
我這里只對(duì) EmailValidator 進(jìn)行說明,因?yàn)?PasswordValidator 也是類似的煤篙。
首先斟览,代碼如下:
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);
}
});
}
這個(gè)類提供了一個(gè) final 的方法(validateEmail),這個(gè)方法其實(shí)返回的是一個(gè) StreamTransformer 實(shí)例
提示
StreamTransformer 的調(diào)用方式為:stream.transform(StreamTransformer)
StreamTransformer 從 Stream 獲取輸入辑奈,然后引用 Stream 的 transform 方法進(jìn)行輸入的處理苛茂,并將處理后的數(shù)據(jù)重新注入到初始的 Stream 中已烤。
在上面的代碼中,處理流程包括根據(jù)一個(gè)正則表達(dá)式檢查輸入的內(nèi)容妓羊,如果匹配則將輸入的內(nèi)容重新注入到 stream 中胯究;如果不匹配,則將錯(cuò)誤信息注入給 stream
4.1.2. 為什么要用 stream.transform()?
如前所述躁绸,如果驗(yàn)證成功裕循,StreamTransformer 會(huì)把輸入的內(nèi)容重新注入回 Stream,具體是怎么運(yùn)作的呢净刮?
我們先看看 Observable.combineLatest3() 這個(gè)方法剥哑,它在每個(gè) Stream 全都拋出至少一個(gè)值之前,并不會(huì)給出任何值
如下圖所示:
- 如果用戶輸入的 email 是有效的淹父,email 的 stream 會(huì)拋出用戶輸入的內(nèi)容星持,同時(shí)再作為 Observable.combineLatest3() 的一個(gè)輸入
- 如果用戶輸入的 email 是無效的,email 的 stream 中會(huì)被添加一條錯(cuò)誤信息(而且 stream 不會(huì)拋出數(shù)據(jù))
- password 和 retype password 也是類似的機(jī)制
- 當(dāng)它們3個(gè)都驗(yàn)證通過時(shí)(也就是 3 個(gè) stream 都拋出了數(shù)據(jù))弹灭,Observable.combineLatest3() 會(huì)借助 (e, p, c) => true 方法拋出一個(gè)
true
值(見代碼第 35 行)
4.1.3. 密碼與重復(fù)密碼驗(yàn)證
我在網(wǎng)上看到有很多關(guān)于密碼與重復(fù)密碼的驗(yàn)證問題,解決方案肯定是有很多的揪垄,這里我針對(duì)其中兩種說明下穷吮。
4.1.3.1. 無錯(cuò)誤提示的基礎(chǔ)方案
第一種解決方案的代碼如下:
Stream<bool> get registerValid => Observable.combineLatest3(
email,
password,
confirmPassword,
(e, p, c) => (0 == p.compareTo(c))
);
這個(gè)解決方案只是在驗(yàn)證了兩個(gè)密碼之后,將它們進(jìn)行比較饥努,如果它們一樣捡鱼,則會(huì)拋出一個(gè) true
值。
等下我們會(huì)看到酷愧,Register 按鈕是否可用是依賴于 registerValid stream 的驾诈,如果兩個(gè)密碼不一樣,registerValid stream 就不會(huì)拋出任何值溶浴,所以 Register 按鈕依然是不可用狀態(tài)乍迄。
但是,用戶不會(huì)接收到任何錯(cuò)誤提示信息士败,所以也不明白發(fā)生了什么闯两。
4.1.3.2. 具有錯(cuò)誤提示的方案
另一種方案是把 confirmPassword stream的處理方法進(jìn)行了擴(kuò)展,代碼如下:
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");
}
});
一旦 retype password 業(yè)務(wù)規(guī)則驗(yàn)證通過谅将, 用戶輸入的內(nèi)容會(huì)被 Stream 拋出漾狼,并調(diào)用 doOnData() 方法,在該方法中通過 _passwordController.value.compareTo() 獲取是否與 password stream 中的數(shù)據(jù)一樣饥臂,如果不一樣逊躁,我們就可用添加錯(cuò)誤提示了。
4.2. RegistrationForm 組件
在解釋說明前我們先來看看 Form 組件的實(shí)現(xiàn)代碼:
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,
);
}),
],
),
);
}
}
說明:
- 因?yàn)?RegisterFormBloc 只是用于表單的驗(yàn)證處理隅熙,所以僅在表單組件中初始化(實(shí)例化)是合適的
- 每個(gè) TextField 都包含在一個(gè)StreamBuilder<String> 中稽煤,以便能夠響應(yīng)驗(yàn)證過程的任何結(jié)果(見代碼中的errorText:snapshot.error)
- 每次 TextField 中輸入的內(nèi)容發(fā)生改變時(shí)核芽,我們都將已輸入的內(nèi)容通過 onChanged:_registrationFormBloc.onEmailChanged (輸入email情況下) 發(fā)送給 BLoC 進(jìn)行驗(yàn)證,
-
RegisterButton 同樣也包含在一個(gè) StreamBuilder<bool> 中
- 如果 _registrationFormBloc.registerValid 拋出了值念脯,onPressed 將在用戶點(diǎn)擊時(shí)對(duì)拋出的值進(jìn)行后續(xù)處理
- 如果沒有值拋出狞洋,onPressed 方法被指定為 null,按鈕會(huì)被置為不可用狀態(tài)
好了绿店!可用看到在表單組件中吉懊,是看不到任何和業(yè)務(wù)規(guī)則相關(guān)的代碼的,這意味著我們可以隨意修改業(yè)務(wù)規(guī)則假勿,而不需要對(duì)表單組件本身進(jìn)行任何修改借嗽,簡直 excellent!