Flutter Stream Bloc

[译]Flutter 响应式编程:Steams 和 BLoC 实

2019-01-11  本文已影响0人  盛开

原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier BoelensReactive Programming - Streams - BLoC 写的后续

阅读本文前建议先阅读前篇,前篇中文翻译有两个版本:

  1. [译]Flutter响应式编程:Streams和BLoC by JarvanMo
    忠于原作的版本

  2. Flutter中如何利用StreamBuilder和BLoC来控制Widget状态 by 吉原拉面
    省略了一些初级概念,补充了一些个人解读

前言

在了解 BLoC, Reactive ProgrammingStreams 概念后,我又花了些时间继续研究,现在非常高兴能够与你们分享一些我经常使用并且个人觉得很有用的模式(至少我是这么认为的)。这些模式为我节约了大量的开发时间,并且让代码更加易读和调试。

目录

(由于原文较长,翻译发布时进行了分割)

  1. BlocProvider 性能优化
    结合 StatefulWidgetInheritedWidget 两者优势构建 BlocProvider

  2. BLoC 的范围和初始化
    根据 BLoC 的使用范围初始化 BLoC

  3. 事件与状态管理
    基于事件(Event) 的状态 (State) 变更响应

  4. 表单验证
    根据表单项验证来控制表单行为 (范例中包含了表单中常用的密码和重复密码比对)

  5. Part Of 模式
    允许组件根据所处环境(是否在某个列表/集合/组件中)调整自身的行为

文中涉及的完整代码可在 GitHub 查看。

4. 表单验证

BLoC 另一个有意思的应用场景就是表单的验证,比如:

下面的例子中,我用了一个名叫 RegistrationForm 的表单,这个表单包含3个 TextField (分别为电子邮箱email、密码password和重复密码 confirmPassword)以及一个按钮 RaisedButton 用来发起注册处理

想要实现的业务规则有:

4.1. RegistrationFormBloc

如前所述,这个 BLoC 负责业务规则验证的处理,实现的代码如下:

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();
  }
}

说明:

好了,我们来深入了解更多的细节…

你可能注意到了,这个 BLoC 类的代码有点特殊,是这样的:

class RegistrationFormBloc extends Object 
                           with EmailValidator, PasswordValidator 
                           implements BlocBase {
  ...
}

使用了 with 关键字表明这个类用到了 MIXINS (一种在另一个类中重用类代码的方法),而且为了使用 with,这个类还需要基于 Object 类进行扩展。这些 mixins 包含了 email 和 password 各自的验证方式。

关于 Mixins 更多信息建议阅读 Romain Rastel 的这篇文章

4.1.1. 表单验证用到的 Mixins

我这里只对 EmailValidator 进行说明,因为 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);
        }
      });
}

这个类提供了一个 final 的方法(validateEmail),这个方法其实返回的是一个 StreamTransformer 实例

提示

StreamTransformer 的调用方式为:stream.transform(StreamTransformer)

StreamTransformerStream 获取输入,然后引用 Streamtransform 方法进行输入的处理,并将处理后的数据重新注入到初始的 Stream 中。

在上面的代码中,处理流程包括根据一个正则表达式检查输入的内容,如果匹配则将输入的内容重新注入到 stream 中;如果不匹配,则将错误信息注入给 stream

4.1.2. 为什么要用 stream.transform()?

如前所述,如果验证成功,StreamTransformer 会把输入的内容重新注入回 Stream,具体是怎么运作的呢?

我们先看看 Observable.combineLatest3() 这个方法,它在每个 Stream 全都抛出至少一个值之前,并不会给出任何值

如下图所示:

Observable.combineLatest3

4.1.3. 密码与重复密码验证

我在网上看到有很多关于密码与重复密码的验证问题,解决方案肯定是有很多的,这里我针对其中两种说明下。

4.1.3.1. 无错误提示的基础方案

第一种解决方案的代码如下:

bloc_password_valid_1.dart

Stream<bool> get registerValid => Observable.combineLatest3(
                                      email, 
                                      password, 
                                      confirmPassword, 
                                      (e, p, c) => (0 == p.compareTo(c))
                                    );

这个解决方案只是在验证了两个密码之后,将它们进行比较,如果它们一样,则会抛出一个 true 值。

等下我们会看到,Register 按钮是否可用是依赖于 registerValid stream 的,如果两个密码不一样,registerValid stream 就不会抛出任何值,所以 Register 按钮依然是不可用状态。

但是,用户不会接收到任何错误提示信息,所以也不明白发生了什么。

4.1.3.2. 具有错误提示的方案

另一种方案是把 confirmPassword stream的处理方法进行了扩展,代码如下:

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 业务规则验证通过, 用户输入的内容会被 Stream 抛出,并调用 doOnData() 方法,在该方法中通过 _passwordController.value.compareTo() 获取是否与 password stream 中的数据一样,如果不一样,我们就可用添加错误提示了。


4.2. RegistrationForm 组件

在解释说明前我们先来看看 Form 组件的实现代码:

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,
                );
              }),
        ],
      ),
    );
  }
}

说明:

好了!可用看到在表单组件中,是看不到任何和业务规则相关的代码的,这意味着我们可以随意修改业务规则,而不需要对表单组件本身进行任何修改,简直 excellent!

上一篇下一篇

猜你喜欢

热点阅读