前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 组件 | ValueListenableBuilder 局部刷新小能手

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

作者头像
张风捷特烈
发布2021-01-04 09:52:01
7.3K0
发布2021-01-04 09:52:01
举报
一、ValueListenableBuilder 的使用

1. ValueListenableBuilder 引言

我们对初始项目非常熟悉,在 _MyHomePageState 中,通过点击按钮将状态量 _counter 自加,在使用 setState 让当前 State 类持有的 Element 进行更新。作为初学者来说,这很方便,也很容易理解。但对于已入门的人来说,这样的 setState 显然是有失优雅的。

setState 会触发本类的 build 方法,我们想要修改的只是一个文字而已,但这样使得 Scaffold 及其之下的元素都被构建了一遍,这会导致 Build 过程出现不必要的逻辑。

解决这一问题方式是四个字:局部刷新。也就是控制 Build 的粒度,只构建刷新的部分。局部刷可以通过 provider 、flutter_bloc 等状态管理库实现。但相对较重,Flutter 框架内部提供了一个非常小巧精致的组件,专门用于局部组件的刷新,它就是 ValueListenableBuilder


2. ValueListenableBuilder 简单使用

现在来看如何使用 ValueListenableBuilder 来优化初始项目,使计数器刷新区域只是数字的范围ValueListenableBuilder 需要传入一个 ValueListenable 对象,它继承自 Listenable ,是一个可监听对象。 ValueListenable 是一个抽象类,不能直接使用, ValueNotifier 是其实现类之一。接收一个泛型,这里需要的是数字,所以泛型用 int

代码语言:javascript
复制
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: [
            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,它是 typedef,本质是一个方法,Widget Function(BuildContext context, T value, Widget child)。每当监听的对象值发生变化时,会触发builder 方法进行刷新。如下,在点击时只需要改变 _counter.value 的值,就会触发 _buildWithValue 从而将界面数字刷新。

代码语言:javascript
复制
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对象,可以被多个 ValueListenableBuilder 监听,这样的话,就可以实现一些梦幻联动。比如下面滑动过程中,中间界面背景底部指示器背景颜色页码示数 都在变化。

左滑

右滑

左滑
左滑
右滑
右滑

我们需要监听 PageView 的滑动,而这个滑动触发频率是非常高的,如果全局刷肯定不好,虽然视觉上体现不明显,但隐患往往就是一点点额外消耗所累加的结果,当最后一根稻草来临时,没有一片雪花是无辜的。通过这个案例,看一下如何局部更新特定的组件,你还会了解 ValueListenableBuilder 中 child 属性 的价值。


1. 主程序

这没什么好说的,主页面组件是 MyHomePage

代码语言:javascript
复制
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 赋值。

代码语言:javascript
复制
class _MyHomePageState extends State<MyHomePage> {
  // 进度监听对象
  final ValueNotifier<double> factor = ValueNotifier<double>(1 / 5);
  // 页数监听对象
  final ValueNotifier<int> page = ValueNotifier<int>(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 来计算两个颜色之间对应分度值的颜色。

代码语言:javascript
复制
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 组件。

代码语言:javascript
复制
@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 组件,根据滑动进度,对子组件进行变换处理。随着滑动不断进行,不断地变换就形成了动画,即下所示:

左滑

右滑

左滑
左滑
右滑
右滑
代码语言:javascript
复制
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 改变,会触发内部重建,从而局部更新页码信息。

代码语言:javascript
复制
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 一半的代码了。

代码语言:javascript
复制
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 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 的优化的根源。

代码语言:javascript
复制
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 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 -- ~ END ~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、ValueListenableBuilder 的使用
    • 1. ValueListenableBuilder 引言
      • 2. ValueListenableBuilder 简单使用
        • 3. 局部刷新的思考
        • 二、ValueListenableBuilder 的 child 属性
          • 1. 主程序
            • 2. 主页状态类
              • 3. 进度条触发刷新
                • 4. 背景的刷新
                  • 5. PageView 的使用及滑动变换动画
                  • 三、ValueListenableBuilder 源码分析
                    • 1. ValueListenableBuilder 类的定义
                      • 2. _ValueListenableBuilderState 类实现
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档