告别setState()! 优雅的UI与Model绑定 Flutter DataBus使用~
作者:Nayuta 链接:https://juejin.im/post/6868104488109604871 本文由作者授权发布。
Flutter开发中,大家都绕不开Widget的刷新,setState()是最简单的用法。但随着当app的交互变得复杂,setState出现的次数便会显著增加,每次setState都会重新调用build方法,这势必对于性能以及代码的可阅读性带来一定的影响。如何优雅的解决这个问题,不得不提到StreamBuilder,StreamBuilder是Flutter中异步构建的核心组件。许多著名的开源框架例如Bloc皆是基于此实现。如果StreamBuilder有了解可以直接看第二部分
一、局部刷新的关键点 StreamBuilder
- setState()
现在页面上有两个数字key1和key2需要展示,当点击上方的按钮时,我们对应修改key1或者key2的值。
img
采用setState()的方式,我们知道很简单,建立本地变量key1,key2,然后放入对应的Text中直接展示。当我们点击按钮时使本地变量key1,key2做增加操作,之后调用setState()。
img
img
img
img
但当我刷新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、统一的管理所有绑定关系的生命周期
- Oracle备库的PDB无法连接的问题(r11笔记第6天)
- Golang语言社区--模板的使用
- Golang语言写Web 应用程序
- 强制wordpress后台HTTPS
- Golang语言 Cookie的使用
- Golang 语言调用动态库实现OpenGL及windows的API编程
- MySQL中的Online DDL(第一篇)(r11笔记第3天)
- 转--quick-cocos做客户端,golang做服务端,实现HTTP通信
- Nginx配置SSL证书
- Golang语言RPC Authorization进行简单ip安全验证的方法
- 深入理解Oracle中的DBCA
- Golang语言goto语句
- 转--Golang语言语法汇总
- Oracle,MySQL迁移整合的问题总结(r10笔记第99天)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- R语言ggsurvplot绘制生存曲线报错 : object of type ‘symbol‘ is not subsettable
- R软件SIR模型网络结构扩散过程模拟
- R语言中使用线性模型、回归决策树自动组合特征因子水平
- R语言缺失值的处理:线性回归模型插补
- R语言如何解决线性混合模型中畸形拟合(Singular fit)的问题
- Android如何在Gradle中更改APK文件名详解
- 面试中常见的 C 语言与 C++ 区别的问题
- Linux系统实现ansible自动化安装配置httpd的方法
- 常用Linux发行版镜像源配置小结
- Linux如何处理文件已删除但空间不释放的问题
- 解析linux或android添加文件系统的属性接口的方法
- linux查看软件的安装位置简单方法
- 使用 bind 设置 DNS 服务器的方法
- Linux jdk安装及环境变量配置教程(jdk-8u144-linux-x64.tar.gz)
- centos6.6 下 安装 php7 + nginx环境的方法