主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green
贡献主题:https://github.com/xitu/juejin-markdown-themes
theme: v-green
highlight:
一、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
。
```dart class _MyHomePageState extends State { // 定义 ValueNotifier 对象 _counter final ValueNotifier _counter = ValueNotifier (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: [ Text( 'You have pushed the button this many times:'), ValueListenableBuilder ( 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
从而将界面数字刷新。
```dart 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
。
```dart 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
赋值。
```dart class _MyHomePageState extends State { // 进度监听对象 final ValueNotifier factor = ValueNotifier (1 / 5); // 页数监听对象 final ValueNotifier page = ValueNotifier (1); // 页面滑动控制器 PageController _ctrl; // 测试组件 色块 final List 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
来计算两个颜色之间对应分度值的颜色。
dart 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 组件。
```dart @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
组件,根据滑动进度,对子组件进行变换处理。随着滑动不断进行,不断地变换就形成了动画,即下所示:
| 左滑 | 右滑 | | ---- | ---- | |||
```dart 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
改变,会触发内部重建,从而局部更新页码信息。
```dart
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
一半的代码了。
```dart class ValueListenableBuilder 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 valueListenable; final ValueWidgetBuilder builder; final Widget child;
@override State createState() => _ValueListenableBuilderState (); }
typedef ValueWidgetBuilder = Widget Function(BuildContext context, T value, Widget child); ```
2. _ValueListenableBuilderState
类实现
对,你没看错,这就是这个组件所有的代码实现
。在 initState
中对传入的可监听对象进行监听,执行 _valueChanged
方法,不出意料 _valueChanged
中进行了 setState
来触发当前状态的刷新。触发 build 方法
,从而触发 widget.builder
回调,这样就实现了局部刷新。可以看到这里回调的 child
是组件传入的 child
,所以直接使用,这就是对 child 的优化的根源。
```dart class _ValueListenableBuilderState extends State > { T value;
@override void initState() { super.initState(); value = widget.valueListenable.value; widget.valueListenable.addListener(_valueChanged); }
@override void didUpdateWidget(ValueListenableBuilder 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 ~