阅读 2061

告别setState()! 优雅的UI与Model绑定 Flutter DataBus使用~

Flutter开发中,大家都绕不开Widget的刷新,setState()是最简单的用法。但随着当app的交互变得复杂,setState出现的次数便会显著增加,每次setState都会重新调用build方法,这势必对于性能以及代码的可阅读性带来一定的影响。如何优雅的解决这个问题,不得不提到StreamBuilder,StreamBuilder是Flutter中异步构建的核心组件。许多著名的开源框架例如Bloc皆是基于此实现。 如果StreamBuilder有了解可以直接看第二部分

一、局部刷新的关键点 StreamBuilder

  • setState()

现在页面上有两个数字key1和key2需要展示,当点击上方的按钮时,我们对应修改key1或者key2的值。 采用setState()的方式,我们知道很简单,建立本地变量key1,key2,然后放入对应的Text中直接展示。当我们点击按钮时使本地变量key1,key2做增加操作,之后调用setState()。

但当我刷新Key1的时候, 会同时重构Key2展示的两个Text,即使我的key2没有发生变化,显然这不是一种合理的做法。

其实Flutter中还提供了一个强大组件SteamBuilder来协助我们处理控件的刷新构建。


  • StreamBuilder

如图,是StreamBuilder使用基本结构,StreamBuidler基于dart中的异步核心之一Stream,采取观察者模式,发送方通过StreamControll发送数据,观察对象接收到数据后构建自己的内容。

从代码可知StreamBuilder接受两个参数,一个stream,表示我们监听的Stream(一个StreamBuilder监听一个Stream,但是一个Stream能被多个Widget监听),builder中传入我们需要构建的contentWidget。

这样Widget的构建完全由Stream触发,控件无需自行setState,它的构建完全由数据驱动,是一种响应式编程。也是许多开源框架例如Bloc等核心原理。


回到上面的例子中,当我们采用StreamBuilder后,上面的例子就变得非常的清晰了,我们建立两条StreamControler,然后把图中的展示key1和key2的两组Text分别由两个StreamBuilder包裹。 在key1的点击事件中往Stream中add数据,这样在key1的流上产生了一条数据,对应的监听者收到数据后,只更新自己的内容,不会重建其他区域。


二、DataLine如何优化StreamBuilder的麻烦使用

经过上面的了解,我们知道。StreamBuilder可以完美解决局部刷新的问题,但StreamBuilder也有着同样明显的缺点,使用起来非常麻烦,需要自己手动创建流,将控件用StreamBuilder包裹构造。

当我们的页面需要多个局部刷新的时候,Stream的编写将会非常麻烦。类似Provide的解决方案也需要设定顶级Widget,然后用consumer包裹子控件,调用更新等等操作。

有没有什么方式可以简化我们的使用呢?

我们注意到,StreamBuilder需要监听一个stream,而这个stream往往来自StreamControler。对于每个StreamControler来说,就像生活中的一条 一对多的数据线数据线(DataLine)一样。 对于这条DataLine,最核心的有两个方法

1、添加观察者(通过StreamBuilder包裹实际展示的contentWidget) : 类似数据线连接手机

2、发送数据 :类似通过数据线给手机充电(因为是一对多的过程)

基于这种思路,设计了一个SingleDataLine,对于这条“数据线"而言,其中T约束了这条线的使用数据类型,currentData能帮助我们拿到当前最新的数据,setData(T t)发送数据。

核心在于我们的addObserver中,该方法需要传入一个 返回值为Widget Function(BuildContext context, T data) observer的方法,这个传入的方法正是我们需要构建的Widget的构造方法。

class SingleDataLine<T> {
  StreamController<T> _stream;

  //拿到当前最新的数据
  T currentData;

  SingleDataLine([T initData]) {
    currentData = initData;
    _stream = initData == null
        ? BehaviorSubject<T>()
        : BehaviorSubject<T>.seeded(initData);
  }

  get outer => _stream.stream;

  get inner => _stream.sink;

  void setData(T t) {
    //同值过滤
    if (t == currentData) return;
    //防止关闭
    if (_stream.isClosed) return;
    currentData = t;
    inner.add(t);
  }

  Widget addObserver(
    Widget Function(BuildContext context, T data) observer,
  ) {
    return DataObserverWidget<T>(this, observer);
  }

  void dispose() {
    _stream.close();
  }
}
复制代码

这个addObserver方法返回一个DataObserverWidget控件,这个组件就是帮我们对StreamBuilder进行了封装,以此简化StreamBuilder的使用。

class _DataObserverWidgetState<T> extends State<DataObserverWidget<T>> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return StreamBuilder(
      stream: widget._dataLine.outer,
      builder: (context, AsyncSnapshot<T> snapshot) {
        if (snapshot != null && snapshot.data != null) {
          print(
              " ${context.widget.toString()} 中的steam接收到了一次数据${snapshot.data}");
          return widget._builder(context, snapshot.data);
        } else {
          return Row();
        }
      },
    );
  }

  @override
  void dispose() {
    super.dispose();
    widget._dataLine.dispose();
  }
复制代码

三、DataBus如何解决多个Stream的绑定

上面我们通过SingDataLine简化了StreamBuilder的使用,但当页面中有多个SingleDataLine的时候,对它的创建和管理,可能会成为一件麻烦的事儿。基于此设计了一个dataBus总线管理。 我们将每一个key和对应的DataLine存入Map中进行管理,通过直接调用getLine(key)的方法获取创建DataLine。而且由于MultDataLine是mixin定义,所以我们可以在任意的类中混入使用方法。例如直接在Widget中混入改类,调用getLine方法获取到StreamBuilder。

import 'package:flutter_dpluse_package/common/widgets/Page/data_line.dart';
mixin MultDataLine {
  final Map<String, SingleDataLine> dataBus = Map();

  SingleDataLine<T> getLine<T>(String key) {
    if (!dataBus.containsKey(key)) {
      SingleDataLine<T> dataLine = new SingleDataLine<T>();
      dataBus[key] = dataLine;
    }
    return dataBus[key];
  }

  void dispose() {
    dataBus.values.forEach((f) => f.dispose());
    dataBus.clear();
  }
}
复制代码

回到上面的例子,使用DataBus,页面的构建将会极其简单,其中核心的发送数据和监听我们通过getLine实现。

ListView(children: <Widget>[
      GestureDetector(
        child: Container(
          width: 150,
          height: 60,
          child: Center(
              child: Text(
            'key1的触发者',
            style: TextStyle(color: Colors.white, fontSize: 20),
          )),
          decoration: BoxDecoration(color: Colors.grey),
        ),
        onTap: () {
            //发送一个数据
          getLine(KEY1).setData(key1++);
        },
      ),
        //绑定监听对象
      getLine(KEY1).addObserver((context, data) {
        //实际的观察者
        return Text(
          'key1当前的数据为 $data',
          style: TextStyle(
              fontSize: 19, color: Colors.green, fontWeight: FontWeight.w600),
        );
      }),
      getLine(KEY1).addObserver(
        (context, data) {
          return Text(
            'key1当前的数据为 $data',
            style: TextStyle(
                fontSize: 19, color: Colors.blue, fontWeight: FontWeight.w600),
          );
        },
      ),]);
复制代码

四、总结

DataBus中使用了StreamBuilder作为构建方式,其实系统中还有一些轻量的观察模式组件可供选择,例如ChangNotify等,但如果单独使用这些组件不可避免观察对象散落在页面中的各个位置,不易于管理。DataBus是个人在开发中实践出一种极简的UI与Model的绑定方法,基于此实现一套普通页面框架,已实践过多个复杂页面。 DataBus核心想解决两个问题:1、简化观察对象与被观察者的绑定 2、统一的管理所有绑定关系的生命周期