[譯]Flutter 響應(yīng)式編程:Steams 和 BLoC 實(shí)踐范例(4) - 表單驗(yàn)證

原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier BoelensReactive Programming - Streams - BLoC 寫的后續(xù)

閱讀本文前建議先閱讀前篇,前篇中文翻譯有兩個(gè)版本:

  1. [譯]Flutter響應(yīng)式編程:Streams和BLoC by JarvanMo
    忠于原作的版本

  2. Flutter中如何利用StreamBuilder和BLoC來控制Widget狀態(tài) by 吉原拉面
    省略了一些初級(jí)概念衬浑,補(bǔ)充了一些個(gè)人解讀

前言

在了解 BLoC, Reactive ProgrammingStreams 概念后逆甜,我又花了些時(shí)間繼續(xù)研究舟肉,現(xiàn)在非常高興能夠與你們分享一些我經(jīng)常使用并且個(gè)人覺得很有用的模式(至少我是這么認(rèn)為的)谴餐。這些模式為我節(jié)約了大量的開發(fā)時(shí)間,并且讓代碼更加易讀和調(diào)試。

目錄

(由于原文較長橘洞,翻譯發(fā)布時(shí)進(jìn)行了分割)

  1. BlocProvider 性能優(yōu)化
    結(jié)合 StatefulWidgetInheritedWidget 兩者優(yōu)勢構(gòu)建 BlocProvider

  2. BLoC 的范圍和初始化
    根據(jù) BLoC 的使用范圍初始化 BLoC

  3. 事件與狀態(tài)管理
    基于事件(Event) 的狀態(tài) (State) 變更響應(yīng)

  4. 表單驗(yàn)證
    根據(jù)表單項(xiàng)驗(yàn)證來控制表單行為 (范例中包含了表單中常用的密碼和重復(fù)密碼比對(duì))

  5. 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)的代碼如下:

bloc_reg_form_bloc.dart

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è) TextFieldStream
  • 提供了 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 也是類似的煤篙。

首先斟览,代碼如下:

bloc_email_validator.dart

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)

StreamTransformerStream 獲取輸入辑奈,然后引用 Streamtransform 方法進(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ì)給出任何值

如下圖所示:

Observable.combineLatest3
  • 如果用戶輸入的 email 是有效的淹父,emailstream 會(huì)拋出用戶輸入的內(nèi)容星持,同時(shí)再作為 Observable.combineLatest3() 的一個(gè)輸入
  • 如果用戶輸入的 email 是無效的,emailstream 中會(huì)被添加一條錯(cuò)誤信息(而且 stream 不會(huì)拋出數(shù)據(jù))
  • passwordretype 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ǔ)方案

第一種解決方案的代碼如下:

bloc_password_valid_1.dart

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ò)展,代碼如下:

bloc_password_valid_2.dart

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)代碼:

bloc_reg_form.dart

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!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市转培,隨后出現(xiàn)的幾起案子恶导,更是在濱河造成了極大的恐慌,老刑警劉巖浸须,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惨寿,死亡現(xiàn)場離奇詭異,居然都是意外死亡删窒,警方通過查閱死者的電腦和手機(jī)裂垦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肌索,“玉大人蕉拢,你說我怎么就攤上這事〕涎牵” “怎么了晕换?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長站宗。 經(jīng)常有香客問我闸准,道長,這世上最難降的妖魔是什么梢灭? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任恕汇,我火速辦了婚禮,結(jié)果婚禮上或辖,老公的妹妹穿的比我還像新娘瘾英。我一直安慰自己,他們只是感情好颂暇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布缺谴。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪湿蛔。 梳的紋絲不亂的頭發(fā)上膀曾,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音阳啥,去河邊找鬼添谊。 笑死,一個(gè)胖子當(dāng)著我的面吹牛察迟,可吹牛的內(nèi)容都是我干的斩狱。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼扎瓶,長吁一口氣:“原來是場噩夢啊……” “哼所踊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起概荷,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤秕岛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后误证,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體继薛,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年愈捅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惋增。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡改鲫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出林束,到底是詐尸還是另有隱情像棘,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布壶冒,位于F島的核電站缕题,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏胖腾。R本人自食惡果不足惜烟零,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咸作。 院中可真熱鬧锨阿,春花似錦、人聲如沸记罚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桐智。三九已至末早,卻和暖如春烟馅,著一層夾襖步出監(jiān)牢的瞬間然磷,已是汗流浹背寡润。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工栗柒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容