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

1,308 阅读6分钟

前言

学习Flutter大概有一年了,没啥太大的成就,一开始是通过玩安卓开发的API配合着别人写好的Demo,自己写了一个玩安卓的App,说写,不如说借鉴。 而后就是年末乘着有时间,自己把公司项目的页面通过Flutter写了一大部分。

其实对于写Flutter,如果注意技术博客的话,大佬们经常会谈这样一个问题:不要有事没事通过setState刷新界面,因为成本高,消耗性能,如果只是页面的某一个控件需要重新绘制,那么就应该精细化控制,进行页面重绘。

然而,我想一直都不得其解,我行我素的使用setState进行页面刷新,说真的,我觉得也挺好的呀。

其实根本原因是我不会Provider,看了半天也没入门。

直到我学习了ValueListenableBuilder。

嗯,上周五写的时候,没注意阅读体验,尽是代码块,我重新编辑了一下,尽量用最少的代码说明业务。

ValueListenableBuilder

纸上得来终觉浅,我们还是从一个简单的例子入手:

一个页面,有一个输入框,和一个按钮,当输入框输入的字符的长度等于6时,按钮改变颜色,并且点击才有响应。 备注一下为了避免无用代码过多,我会把公共部分代码放在文章最后,这样避免过多的非主题代码影响阅读。 关键代码的注释我写在代码块中了。

我们先来一个setState版本:

class ExampleWithSetState extends StatefulWidget {
  @override
  _ExampleWithSetStateState createState() => _ExampleWithSetStateState();
}

class _ExampleWithSetStateState extends State<ExampleWithSetState> {
  var _isOK = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Scaffold(
        appBar: AppBar(
          title: Text("一个输入绑定一个按钮"),
        ),
        body: Container(
          margin: const EdgeInsets.fromLTRB(16, 40, 16, 0),
          child: ListView(
            children: [
              _textField(
                  title: "输入字符串",
                  limit: 6,
                  onChanged: (inputString) {
                  
                  
                    /// 通过setState进行整体页面刷新
                    setState(() {
                      _isOK = inputString.length == 6;
                    });
                    
                    
                  }),
              Container(
                margin: const EdgeInsets.fromLTRB(0, 60, 0, 0),
                child: RaisedButton(
                  color: _buttonColor(_isOK),
                  shape: BeveledRectangleBorder(
                    borderRadius: BorderRadius.circular(0),
                  ),
                  child: Container(
                    child: Center(
                      child: Text(
                        "确认",
                        style: TextStyle(
                          fontSize: 16,
                          color: _isOK ? Colors.white : Color(0xFFA3A4A4),
                        ),
                      ),
                    ),
                    height: 48,
                  ),
                  onPressed: () {
                    if (_isOK) {
                      print("按钮可以用了");
                    }
                  },
                ),
              ),
            ],
          ),
        ),
      ),
      onTap: () => print("键盘收起方法"),
    );
  }
}

然后是ValueListenableBuilder版本:

class ExampleWithValueListenableBuilder extends StatelessWidget {
  final _isOKNotifier = ValueNotifier(false);

  ExampleWithValueListenableBuilder({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Scaffold(
        appBar: AppBar(
          title: Text("ValueListenableBuilder一个输入绑定一个按钮"),
        ),
        body: Container(
          margin: const EdgeInsets.fromLTRB(16, 40, 16, 0),
          child: ListView(
            children: [
              _textField(
                  title: "输入字符串",
                  limit: 6,
                  onChanged: (inputString) {
                  
                  
                    /// 通过对_isOKNotifier进行赋值局部更新ValueListenableBuilder里面的Widget
                    _isOKNotifier.value = inputString.length == 6;
                    
                    
                  }),
              ValueListenableBuilder(
                valueListenable: _isOKNotifier,
                builder: (context, isOK, child) {
                  return Container(
                    margin: const EdgeInsets.fromLTRB(0, 60, 0, 0),
                    child: RaisedButton(
                      color: _buttonColor(isOK),
                      shape: BeveledRectangleBorder(
                        borderRadius: BorderRadius.circular(0),
                      ),
                      child: Container(
                        child: Center(
                          child: Text(
                            "确认",
                            style: TextStyle(
                              fontSize: 16,
                              color: isOK ? Colors.white : Color(0xFFA3A4A4),
                            ),
                          ),
                        ),
                        height: 48,
                      ),
                      onPressed: () {
                        if (isOK) {
                          print("按钮可以用了");
                        }
                      },
                    ),
                  );
                },
              ),
            ],
          ),
        ),
      ),
      onTap: () => print("键盘收起方法"),
    );
  }
}

我强烈建议不是很了解ValueListenableBuilder使用的大佬们CV运行跑一把,你会发现他们两实现的功能是一样 而两个例子最大的区别在哪里,ExampleWithSetState继承是StatefulWidget,而ExampleWithValueListenableBuilder继承的是StatelessWidget。 ExampleWithValueListenableBuilder的优势有下面两点:

  1. 不可变比可变要可靠的多(这是Swift那一套)。
  2. 通过仅对button做变化的精细化控制,我们让性能上去了。

虽然看起来这点性能可有可无,不过在思路上的扩展才是最重要的,不是所有的数据更新都必须用setState来自己主动控制。

有关ValueListenableBuilder的使用,我个人建议看看大佬的文章与使用,肯定比我的一言半语强很多:Flutter 组件 | ValueListenableBuilder 局部刷新小能手

这里例子里面,我们仅仅是一个输入框与一个按钮进行绑定。

不过日常开发中,多个输入与一个按钮绑定的业务场景比比皆是,甚至更复杂的业务场景,如果我们一味使用setState,一来会很复杂,二来消耗性能,而使用ValueListenableBuilder,貌似只能一个变量绑定一个Widget。

不过其实ValueListenableBuilder是可以进行嵌套的,下面的例子就是如此:

class ExampleWithTwoValueListenableBuilder extends StatelessWidget {
  final _isOK1Notifier = ValueNotifier(false);

  final _isOK2Notifier = ValueNotifier(false);

  ExampleWithTwoValueListenableBuilder({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: Scaffold(
        appBar: AppBar(
          title: Text("两个输入绑定一个按钮"),
        ),
        body: Container(
          margin: const EdgeInsets.fromLTRB(16, 40, 16, 0),
          child: ListView(
            children: [
              _textField(
                  title: "输入11位字符串",
                  limit: 11,
                  onChanged: (inputString) {
                    _isOK1Notifier.value = inputString.length == 11;
                  }),
              _textField(
                  title: "输入6位字符串",
                  limit: 6,
                  onChanged: (inputString) {
                    _isOK2Notifier.value = inputString.length == 6;
                  }),
              ValueListenableBuilder(
                valueListenable: _isOK1Notifier,
                builder: (context, isOK1, child) {
                  return ValueListenableBuilder(
                    valueListenable: _isOK2Notifier,
                    builder: (context, isOK2, child) {
                      final isOK = isOK1 && isOK2;
                      return Container(
                        margin: const EdgeInsets.fromLTRB(0, 60, 0, 0),
                        child: RaisedButton(
                          color: _buttonColor(isOK),
                          shape: BeveledRectangleBorder(
                            borderRadius: BorderRadius.circular(0),
                          ),
                          child: Container(
                            child: Center(
                              child: Text(
                                "确认",
                                style: TextStyle(
                                  fontSize: 16,
                                  color:
                                      isOK ? Colors.white : Color(0xFFA3A4A4),
                                ),
                              ),
                            ),
                            height: 48,
                          ),
                          onPressed: () {
                            if (isOK) {
                              print("按钮可以用了");
                            }
                          },
                        ),
                      );
                    },
                  );
                },
              ),
            ],
          ),
        ),
      ),
      onTap: () => print("键盘收起方法"),
    );
  }
}

嵌套代码关键在于下面这段:



  /// 在ValueListenableBuilder中又嵌套了一个ValueListenableBuilder
  /// 保证2个需要监听的变量在最里层的builder中可以合成需要两个变量绑定的Widget
  
  
  ValueListenableBuilder(
    valueListenable: _isOK1Notifier,
    builder: (context, isOK1, child) {
      return ValueListenableBuilder(
        valueListenable: _isOK2Notifier,
        builder: (context, isOK2, child) {
          final isOK = isOK1 && isOK2;
          return Container(
            margin: const EdgeInsets.fromLTRB(0, 60, 0, 0),
            child: RaisedButton(
              color: _buttonColor(isOK),
              shape: BeveledRectangleBorder(
                borderRadius: BorderRadius.circular(0),
              ),
              child: Container(
                child: Center(
                  child: Text(
                    "确认",
                    style: TextStyle(
                      fontSize: 16,
                      color:
                          isOK ? Colors.white : Color(0xFFA3A4A4),
                    ),
                  ),
                ),
                height: 48,
              ),
              onPressed: () {
                if (isOK) {
                  print("按钮可以用了");
                }
              },
            ),
          );
        },
      );
    },
   ),

你可以试想一下,如果有三个或者四个输入需要和一个按钮进行绑定,那么你可能就会陷入无限嵌套的地狱之中。这个时候Provider就该出场了。

到这里,我们可以先总结一下ValueListenableBuilder的使用场景:

  1. 通过定义ValueNotifier与ValueListenableBuilder传参关联,可以进行数据与组件的绑定。
  2. 绑定关系在使用的时候可以做精细化控制,可以仅在需要的地方进行重绘,进而到达性能上的提升。
  3. 一般情况下,ValueListenableBuilder一对一的绑定关系是比较好控制与书写代码的,如果涉及到多对一的绑定,ValueListenableBuilder会陷入嵌套地狱。

抽出来的非业务代码

将下面这些代码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);
    }
  }

下节我们先从MultiProvider开始说起,至于什么全局的状态管理,我看我够不够勤快吧。。。今天先撤退。

另外,ValueNotifier总是让我联想到了SwiftUI中的ObservableObject协议,唔,脑壳疼。

本文正在参与「掘金 2021 春招闯关活动」, 点击查看活动详情