likes
comments
collection
share

Flutter:从ValueListenableBuilder到Provider(二)|技术点评

作者站长头像
站长
· 阅读数 0

上一节简单回顾

在上一篇文章里面,主要是讲的ValueListenableBuilder的基本使用,我个人认为ValueNotifier与ValueListenableBuilder在一对一的绑定(一个变量对于一个Widget)中,简单易用,容易理解,而如果在多对一的绑定中,使用ValueListenableBuilder可能会陷入嵌套的地狱之中,所以这个时候就是MultiProvider登场了。

往期文章:

MultiProvider

说到MultiProvider,它其实是provider库中,针对多状态绑定一个Widget的一个组件。 说到provider库我们总会与全局状态管理联系在一起,孰不知,其实provider在单页面管理上面也非常好的,甚至可以说,理解了provider在单页面管理,那么全局管理不在话下。

那么我们还是先接着上一节的文末最后的例子,接着写代码。 需求如下:两个输入框,第一个输入框输入11位手机号并且第二个输入框输入6位验证码时,按钮变色且响应点击事件。

在写组件代码之前,我们先定义下面两个模型:

class IsRightPhoneNumber extends ChangeNotifier {
  bool _isOK = false;

  bool get isOK => _isOK;

  set isOK(bool newValue) {
    _isOK = newValue;
    notifyListeners();
  }
}

class IsRightCode extends ChangeNotifier {
  bool _isOK = false;

  bool get isOK => _isOK;

  set isOK(bool newValue) {
    _isOK = newValue;
    notifyListeners();
  }
}

也许你会好奇,为啥要写这两个模型,这个两个模型又有何用?上一节里面,我们并没有深入说ValueNotifier的源码,那么我们现在去看看ValueNotifier的源码:

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  /// Creates a [ChangeNotifier] that wraps this value.
  ValueNotifier(this._value);

  /// The current value stored in this notifier.
  ///
  /// When the value is replaced with something that is not equal to the old
  /// value as evaluated by the equality operator ==, this class notifies its
  /// listeners.
  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

如果你仔细看,就会发现,我们定义的模型和ValueNotifier用异曲同工之妙,都继承了ChangeNotifier,而ChangeNotifier中最最重要的方法就是notifyListeners(),它干嘛呢?它就是通知告诉界面,值变化了,要刷新界面了。

可以说我们自定义的模型是ValueNotifier的简化与特化版,简化是因为我们并没有遵守ValueListenable协议,特化是因为我们将ValueNotifier中泛型指定成为我们定义的类型。 当然自定义的模型直接继承ValueNotifier来进行编写也是可以的。

下面进入界面的主要代码:

和上一节一样,其他的非主题逻辑的代码我会附在最后。

  /// 定义两个需要监听的全局变量
  final _isRightPhoneNumber = IsRightPhoneNumber();

  final _isRightCode = IsRightCode();

  @override
  Widget build(BuildContext context) {
  
    /// 构建页面的顶层我们使用了MultiProvider组件,并注册监听_isRightPhoneNumber与_isRightCode
  
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (context) => _isRightPhoneNumber,
        ),
        ChangeNotifierProvider(
          create: (context) => _isRightCode,
        ),
      ],
      child: GestureDetector(
        child: Scaffold(
          appBar: AppBar(
            title: Text("MultiProvider两个输入绑定一个按钮"),
          ),
          body: Container(
            margin: const EdgeInsets.fromLTRB(16, 40, 16, 0),
            child: ListView(
              children: [
                _textField(
                    title: "输入11位字符串",
                    limit: 11,
                    onChanged: (inputString) {
                      _isRightPhoneNumber.isOK = inputString.length == 11;
                    }),
                _textField(
                    title: "输入6位字符串",
                    limit: 6,
                    onChanged: (inputString) {
                      _isRightCode.isOK = inputString.length == 6;
                    }),
                    
                    
                /// 使用provider库中的Selector组件
                /// 首先不要被<IsRightPhoneNumber, IsRightCode, bool>这三个泛型瞎坏了,
                /// 它实际是定义两个入参分别为IsRightPhoneNumber和IsRightCode类型,返回bool类型,在Selector2中的selecto进行使用
                
                
                Selector2<IsRightPhoneNumber, IsRightCode, bool>(
                  selector: (context, value1, value2) =>
                      value1.isOK && value2.isOK,
                  builder: (context, isAllRight, child) {
                    return Container(
                      margin: const EdgeInsets.fromLTRB(0, 30, 0, 0),
                      child: RaisedButton(
                        color: _buttonColor(isAllRight),
                        shape: BeveledRectangleBorder(
                          borderRadius: BorderRadius.circular(0),
                        ),
                        child: Container(
                          child: Center(
                            child: Text(
                              "Selector2+自定义模型 确认修改",
                              style: TextStyle(
                                fontSize: 16,
                                color: isAllRight
                                    ? Colors.white
                                    : DSColor.colorA3A4A4,
                              ),
                            ),
                          ),
                          height: 48,
                        ),
                        onPressed: () {
                          if (isAllRight) {}
                        },
                      ),
                    );
                  },
                ),
              ],
            ),
          ),
        ),
        onTap: () => print("键盘收起方法"),
      ),
    );
  }

代码解读:

一个关键点是构建页面的顶层我们使用了MultiProvider组件,并注册监听_isRightPhoneNumber与_isRightCode

另一个关键是Selector2组件的使用,首先不要被<IsRightPhoneNumber, IsRightCode, bool>这三个泛型吓坏了,它实际是定义两个入参分别为IsRightPhoneNumber和IsRightCode类型,返回bool类型,在Selector2中的selector进行使用,而Selector2中的builder其实和ValueListenableBuilder的builder非常相似,就是通过变量去创建Widget,并且在变量变化的时候去改变Widget,而这个变量实际是由selector进行控制,selector的在本例子中本质类型是一个bool Function(IsRightPhoneNumber, IsRightCode)的函数而已。

Selector与Consumer

Selector2是专门处理有两个入参,合并为一个新的出参的组件,另还有Selector3、Selector4、Selector5、Selector6这样的组件可以使用,分别对应的意思通过类名最后追加的数字应该就可以理解吧。

除了Selector系列组件,还有一个Consumer组件,我理解它其实就是Selector简化版本,入参少了selector和shouldRebuild参数,它的builder方法中会直接上按顺序定义好的泛型参数,进行Widget的创建与更新。这里我们的按钮也完全可以使用Consumer2来进行构建,其代码如下:

Consumer2<IsRightPhoneNumber, IsRightCode>(
  builder: (context, IsRightPhoneNumber value1, IsRightCode value2, child) {
    final isAllRight = value1.isOK && value2.isOK;
    return Container(
      margin: const EdgeInsets.fromLTRB(0, 30, 0, 0),
      child: RaisedButton(
        color: _buttonColor(isAllRight),
        shape: BeveledRectangleBorder(
          borderRadius: BorderRadius.circular(0),
        ),
        child: Container(
          child: Center(
            child: Text(
              "Consumer2+自定义模型 确认修改",
              style: TextStyle(
                fontSize: 16,
                color: isAllRight
                    ? Colors.white
                    : DSColor.colorA3A4A4,
              ),
            ),
          ),
          height: 48,
        ),
        onPressed: () {
          if (isAllRight) {}
        },
      ),
    );
  },
),

整体而言,不管是Selector还是Consumer,在使用方法上和ValueListenableBuilder基本一致,而Selector的精细程度上更上一层楼,除了入参有selector方法,用来展平多入参统一到唯一变量进行Widget的更新外,另外还有shouldRebuild参数来判断新值与旧值是否真的需要进行Widget更新,整体而言,对于单页面的多参数控制Widget,无论是从编码与易用性上看,对于开发者都是十分友好的。

抽出来的非业务代码

将下面这些代码CV到每个例子的类中就可以了。

  Widget _textField({
    String title,
    int limit,
    ValueChanged<String> onChanged,
  }) {
    return Column(
      children: [
        TextField(
          inputFormatters: [LengthLimitingTextInputFormatter(limit)],
          decoration: InputDecoration(
            hintText: title,
            hintStyle: TextStyle(
              color: Color(0xFFA3A4A4),
              fontSize: 14,
            ),
            enabledBorder: UnderlineInputBorder(
              // 不是焦点的时候颜色
              borderSide: BorderSide(color: Color(0xFF303131)),
            ),
            focusedBorder: UnderlineInputBorder(
              // 焦点集中的时候颜色
              borderSide: BorderSide(color: Color(0xFFC3B5AB)),
            ),
          ),
          keyboardType: TextInputType.number,
          onChanged: onChanged,
        ),
        _spacer23(),
      ],
    );
  }

  Widget _spacer23() {
    return SizedBox(
      height: 23,
    );
  }

  Color _buttonColor(bool value) {
    if (value) {
      return Color(0xFFC3B5AB);
    } else {
      return Color(0xFF303131);
    }
  }

下一节,聊一下我对全局管理的理解吧,嗯,之前掉坑里去了,现在也没爬起来。。。

参考文档: