一、ValueListenableBuilder 的使用
1. ValueListenableBuilder 引言
我们对初始项目非常熟悉,在 _MyHomePageState
中,通过点击按钮将状态量 _counter
自加,在使用 setState
让当前 State 类持有的 Element 进行更新。作为初学者来说,这很方便,也很容易理解。但对于已入门的人来说,这样的 setState
显然是有失优雅的。
setState
会触发本类的 build
方法,我们想要修改的只是一个文字而已,但这样使得 Scaffold
及其之下的元素都被构建了一遍,这会导致 Build
过程出现不必要的逻辑。
解决这一问题方式是四个字:局部刷新
。也就是控制 Build 的粒度,只构建刷新的部分。局部刷可以通过 provider 、flutter_bloc 等状态管理库实现。但相对较重,Flutter 框架内部提供了一个非常小巧精致的组件,专门用于局部组件的刷新,它就是 ValueListenableBuilder
。
2. ValueListenableBuilder 简单使用
现在来看如何使用 ValueListenableBuilder
来优化初始项目,使计数器刷新区域只是数字的范围
。
ValueListenableBuilder
需要传入一个 ValueListenable<T>
对象,它继承自 Listenable<T>
,是一个可监听对象。 ValueListenable<T>
是一个抽象类,不能直接使用, ValueNotifier
是其实现类之一。接收一个泛型,这里需要的是数字,所以泛型用 int
。
class _MyHomePageState extends State<MyHomePage> {
// 定义 ValueNotifier 对象 _counter
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
@override
void dispose() {
_counter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( title: Text(widget.title), ),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text( 'You have pushed the button this many times:'),
ValueListenableBuilder<int>(
builder: _buildWithValue,
valueListenable: _counter,
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}
ValueListenableBuilder 还需要一个 builder
,对应的类型为 ValueWidgetBuilder<T>
,它是 typedef
,本质是一个方法,Widget Function(BuildContext context, T value, Widget child)
。每当监听的对象值发生变化时,会触发builder
方法进行刷新。如下,在点击时只需要改变 _counter.value
的值,就会触发 _buildWithValue
从而将界面数字刷新。
void _incrementCounter() {
_counter.value += 1;
}
Widget _buildWithValue(BuildContext context, int value, Widget child) {
return Text(
'$value',
style: Theme.of(context).textTheme.headline4,
);
}
3. 局部刷新的思考
这样就实现了局部刷新,可以看出 Build 的时间少了很多,比起之前的全面刷新就会有所优化。注意,这里的很多帧是由于 FloatingActionButton
的水波纹效果。界面的变化是果
,帧的刷新是 因
。
我们反过来想想 FloatingActionButton
表象状态会自己变化,不然是不会出现水波纹的,那么在点击时,它底层实现的某处必然执行 setState
,但 FloatingActionButton
是一个 StatelessWidget
,为什么界面有变化的能力? 原因很简单 ,因为它内部使用了 RawMaterialButton
,它是 StatefulWidget
。水波纹的效果也是在 RawMaterialButton
被点击时通过 setState
来刷新实现的。这也是另一种局部刷新实现的方式:组件分离
,将状态变化的刷新封装在组件内部,向外界提供操作接口。这样一方面,用户不需要自己实现复杂的状态变化效果。另一方面,自己状态的变化仅在本组件状态内部,不会影响外界范围,即 局部刷新
。
二、ValueListenableBuilder 的 child 属性
可以说 ValueListenableBuilder
是一个非常好用的组件,它可以监听一个值的变化来构建组件,可以说是一把低耗狙击枪, 指哪打哪
。更强大的是一个 ValueListenable<T>
对象,可以被多个 ValueListenableBuilder
监听,这样的话,就可以实现一些梦幻联动。比如下面滑动过程中,中间界面背景
、底部指示器
、背景颜色
、页码示数
都在变化。
左滑 | 右滑 |
---|---|
我们需要监听
PageView 的滑动,而这个滑动触发频率是非常高
的,如果全局刷肯定不好,虽然视觉上
体现不明显,但隐患往往就是一点点额外消耗所累加的结果
,当最后一根稻草来临时,没有一片雪花是无辜的。通过这个案例,看一下如何局部更新特定的组件,你还会了解 ValueListenableBuilder 中 child 属性
的价值。
1. 主程序
这没什么好说的,主页面组件是 MyHomePage
。
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
2. 主页状态类
这里用了两个可监听对象 factor
和 page
分别处理滑动进度变化
和页码数变化
。其实只用 factor
也可以算出当前页码,但是 factor
更新的频率很高,而页码的变化只在切页时变化,所以加一个 page
变量会更好。在 initState
中对 页面滑动控制器
进行初始化,并监听变化,为 factor
赋值。
class _MyHomePageState extends State<MyHomePage> {
// 进度监听对象
final ValueNotifier<double> factor = ValueNotifier<double>(1 / 5);
// 页数监听对象
final ValueNotifier<int> page = ValueNotifier<int>(1);
// 页面滑动控制器
PageController _ctrl;
// 测试组件 色块
final List<Widget> testWidgets =
[Colors.red, Colors.yellow, Colors.blue, Colors.green, Colors.orange]
.map((e) => Container(
decoration: BoxDecoration(
color: e,
borderRadius: BorderRadius.all(
Radius.circular(20),
))))
.toList();
Color get startColor => Colors.red; // 起点颜色
Color get endColor => Colors.blue; // 终点颜色
//圆角装饰
BoxDecoration get boxDecoration => const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(40), topRight: Radius.circular(40)));
// 初始化
@override
void initState() {
super.initState();
_ctrl = PageController(
viewportFraction: 0.9,
)..addListener(() {
double value = (_ctrl.page + 1) % 5 / 5;
factor.value = value == 0 ? 1 : value;
});
}
// 释放对象
@override
void dispose() {
_ctrl.dispose();
page.dispose();
factor.dispose();
super.dispose();
}
3. 进度条触发刷新
先看一下底部的进度条,我们需要的就是在滑动到特定的分度值时,通知 LinearProgressIndicator
进行变化。这便是 ValueListenableBuilder
的长处,通过监听 factor
,每当滑动时 factor.value
改变时,就会 定点刷新这个进度条
。这便是使用 ValueListenableBuilder
的妙处。另 外颜色可以通过 Color.lerp
来计算两个颜色之间对应分度值的颜色。
Widget _buildProgress() => Container(
margin: EdgeInsets.only(bottom: 12, left: 48, right: 48, top: 10),
height: 2,
child: ValueListenableBuilder(
valueListenable: factor,
builder: (context, value, child) {
return LinearProgressIndicator(
value: factor.value,
valueColor: AlwaysStoppedAnimation(
Color.lerp(startColor, endColor, factor.value,),
),
);
},
),
);
4. 背景的刷新
关于背景的刷新,有点小门道。这里会体现出 ValueListenableBuilder中child
属性的作用。 主页内容放入 child 属性中,那么在触发 builder
时,会直接使用这个 child,不会再构建一遍 child
。比如,现在当进度刷新时,不会触发 _buildTitle
方法,这说明 tag2 之下的组件没有被构建
。如果将 tag2
的组件整体放到 tag1 的child
处时,那么伴随刷新, _buildTitle
方法会不断触发。这就是 child
属性的妙处。这点和 AnimatedBuilder
是一致的。当然你可以用 Stack 来叠放背景,不过这样感觉多此一举,还要额外搭上个 Stake 组件。
@override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder(
valueListenable: factor,
builder: (_, value, child) => Container(
color: Color.lerp(startColor, endColor, value),
child: child, //<--- tag1
),
child: Container( //<--- tag2
child: Column(
children: [
_buildTitle(context),
Expanded( child: Container( child: _buildContent(),
margin: const EdgeInsets.only(left: 8, right: 8),
decoration: boxDecoration,
))
],
),
),
),
);
}
Widget _buildTitle(BuildContext context) {
print('---------_buildTitle------------');
return Container(
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.25,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.api, color: Colors.white, size: 45,),
SizedBox(width: 20,),
ValueListenableBuilder(
valueListenable: page,
builder: _buildWithPageChange,
),
],
),
);
}
5. PageView 的使用及滑动变换动画
主题内容通过 _buildContent
进行构建。PageView
在 onPageChanged
中触发 page.value
的变化。这里的两点在于使用 AnimatedBuilder
对每个 item 在滑动过程中进行变换动画。AnimatedBuilder
的监听对象就是 页面滑动控制器 _ctrl
,它也是一个可监听对象。注意这里将与变换无关的构建放在 AnimatedBuilder 的 child 属性中
,和上面是异曲同工的。通过 _buildAnimOfItem
方法使用 Transform
组件,根据滑动进度,对子组件进行变换处理。随着滑动不断进行,不断地变换就形成了动画,即下所示:
左滑 | 右滑 |
---|---|
Widget _buildContent() {
return Container(
padding: EdgeInsets.only(bottom: 80, top: 40),
child: Column(
children: [
Expanded(
child: PageView.builder(
onPageChanged: (index) => page.value = index + 1,
controller: _ctrl,
itemCount: testWidgets.length,
itemBuilder: (_, index) => AnimatedBuilder(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: testWidgets[index],
),
animation: _ctrl,
builder: (context, child) => _buildAnimOfItem(context, child, index)),
),
),
_buildProgress(),
],
));
}
Widget _buildAnimOfItem(BuildContext context, Widget child, int index) {
double value;
if (_ctrl.position.haveDimensions) {
value = _ctrl.page - index;
} else {
value = index.toDouble();
}
value = (1 - ((value.abs()) * .5)).clamp(0, 1).toDouble();
value = Curves.easeOut.transform(value);
return Transform(
transform: Matrix4.diagonal3Values(1.0, value, 1.0),
alignment: Alignment.center,
child: child,
);
}
顶部的页码标识,可以通过 ValueListenableBuilder
来监听 page
,切页时 page
改变,会触发内部重建,从而局部更新页码信息。
Widget _buildTitle(BuildContext context) {
return Container(
alignment: Alignment.center,
height: MediaQuery.of(context).size.height * 0.25,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.api, color: Colors.white, size: 45,),
SizedBox(width: 20,),
ValueListenableBuilder( <---
valueListenable: page,
builder: _buildWithPageChange,
),
],
),
);
}
Widget _buildWithPageChange(BuildContext context, int value, Widget child) {
return Text(
"绘制集录 $value/5",
style: TextStyle(fontSize: 30, color: Colors.white),
);
}
到这里,你应该对 ValueListenableBuilder
的价值有了很清楚的认识,它就是监听值的变化进行局部刷新
。 ValueListenableBuilder
这么好用,源码应该非常复杂吧。其实它的核心代码不到 50 行。
三、ValueListenableBuilder 源码分析
1. ValueListenableBuilder 类的定义
继承自 StatefulWidget
,定义 final 成员变量,通过 _ValueListenableBuilderState
实现构建。这些常规操作没什么难的,这样你就看完了 ValueListenableBuilder
一半的代码了。
class ValueListenableBuilder<T> extends StatefulWidget {
const ValueListenableBuilder({
Key key,
@required this.valueListenable,
@required this.builder,
this.child,
}) : assert(valueListenable != null),
assert(builder != null),
super(key: key);
final ValueListenable<T> valueListenable;
final ValueWidgetBuilder<T> builder;
final Widget child;
@override
State<StatefulWidget> createState() => _ValueListenableBuilderState<T>();
}
typedef ValueWidgetBuilder<T> = Widget Function(BuildContext context, T value, Widget child);
2. _ValueListenableBuilderState
类实现
对,你没看错,这就是这个组件所有的代码实现
。在 initState
中对传入的可监听对象进行监听,执行 _valueChanged
方法,不出意料 _valueChanged
中进行了 setState
来触发当前状态的刷新。触发 build 方法
,从而触发 widget.builder
回调,这样就实现了局部刷新。可以看到这里回调的 child
是组件传入的 child
,所以直接使用,这就是对 child 的优化的根源。
class _ValueListenableBuilderState<T> extends State<ValueListenableBuilder<T>> {
T value;
@override
void initState() {
super.initState();
value = widget.valueListenable.value;
widget.valueListenable.addListener(_valueChanged);
}
@override
void didUpdateWidget(ValueListenableBuilder<T> oldWidget) {
if (oldWidget.valueListenable != widget.valueListenable) {
oldWidget.valueListenable.removeListener(_valueChanged);
value = widget.valueListenable.value;
widget.valueListenable.addListener(_valueChanged);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
widget.valueListenable.removeListener(_valueChanged);
super.dispose();
}
void _valueChanged() {
setState(() { value = widget.valueListenable.value; });
}
@override
Widget build(BuildContext context) {
return widget.builder(context, value, widget.child);
}
}
可以看到 ValueListenableBuilder
实现局部刷新的本质,也是进行组件的抽离
,让组件状态的改变框定在状态内部,并通过 builder 回调控制局部刷新,暴露给用户使用,只能说一个字,妙。
@张风捷特烈 2020.12.30 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~