Flutter 组件 | ValueListenableBuilder 局部刷新小能手


主题列表: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. 主页状态类

这里用了两个可监听对象 factorpage 分别处理滑动进度变化页码数变化。其实只用 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 进行构建。PageViewonPageChanged 中触发 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 ~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值