Flutter局部刷新 ValueNotifier和ValueListenableBuilder

1.介绍

在上一篇中Provider,我们介绍了一个Widget Selector,它的目的是为了减少setState((){})带来的全局刷新问题,对于复杂的页面来说,如果仅仅只是其中一小块发生改变,就触发大面积的刷新,很大可能会带来很大的性能问题以及耗电问题 
因此官方提供了`ValueNotifier`来进行刷新。ValueNotifier不仅仅可以进行单个组件的刷新,也可以同时被多个组件进行监听,还可以对自定义数据进行处理

适用范围:适用某个对象(int/float/double或者Bean对象)ValueNotifier是轻量级的状态管理组件,适用于一些常用小组件的封装

2.如何使用

监听器:

ValueNotifier<int> _valueNotifier = ValueNotifier<int>(0);//数据监听器

被监听的地方

ValueListenableBuilder<int>(
      valueListenable: _valueNotifier,
      builder: (BuildContext context, int value, Widget child){
        print('YM----ChildWidget1-----ValueListenableBuilder----进行刷新啊');
        return Text('ChildWidget1新值:$value');
      },
      // child: Container(
      //   child: Text('子控件'),
      // ),
    );

属性解答:

  • builder 在数据发生变化时适用
  • valueListenable 监听器
  • child:会回传给builder中的child,可以为null
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Material App Bar'),
        ),
        body: Center(
          child: Container(
            child: Column(
              children: [
                MyHomePage(
                  title: "ValueNotifier",
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  MyHomePage({Key key, this.title}) : super(key: key);

  @override
  _MyHomePageState createState() {
    return _MyHomePageState();
  }
}

class _MyHomePageState extends State<MyHomePage> {
  // 1.定义 ValueNotifier 对象 _counter
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);

  @override
  void dispose() {
    _counter.dispose();
    super.dispose();
  }

  void add() {
    _counter.value += 1;
  }

  Widget _buildValue(BuildContext context, int value, Widget child) {
    print("_buildValue2刷新了");
    return Text(
      '$value',
      style: Theme.of(context).textTheme.headline4,
    );
  }


  Widget _buildValue2(BuildContext context, int value, Widget child) {
    print("_buildValue2刷新了");
    return Text(
      '$value',
      style: Theme.of(context).textTheme.headline4,
    );
  }

  @override
  Widget build(BuildContext context) {
    print("重新刷新了");
    return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ValueListenableBuilder<int>(
              builder: _buildValue,
              valueListenable: _counter,
            ),
            ValueListenableBuilder<int>(
              builder: _buildValue2,
              valueListenable: _counter,
            ),
            FloatingActionButton(
              onPressed: add,
              child: Icon(Icons.add),
            ),
          ],
        ),
      );
  }
}

解答:ValueNotifier本质还是ChangeNotifier(类比Android ViewModel和LiveData),ValueListenableBuilder 还需要一个 builder,对应于Provider中Consumer

每当监听的对象值发生变化时,会触发builder 方法进行刷新。在点击时只需要改变 _counter.value 的值,就会触发 _buildWithValue 从而将界面数字刷新。

我们这样就减少了很对绘制的时间,并提升了绘制性能

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  ValueNotifier(this._value);
  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

3.局部刷新的思考

问题1:多个StatefulWidget包裹时,SA包裹SB,SB包裹SC,例如,许多Button都有水波纹以及闪烁的颜色,此时为什么没有触发全局的刷新,Button肯定执行了内部的SetState来刷新实现的。

解答:这是一种组件分离的效果,SC(Button)中setState,状态变化的刷新封装在自己组件内,向外界提供操作接口,则只会影响到本身的组件,父组件不会影响到,如果在父组件SetState,则影响到父组件以及父组件下的所有子组件。我们专门来处理这种情况。

4.关于 child的理解

关于背景的刷新,有点小门道。这里会体现出 ValueListenableBuilder中child 属性的作用。 主页内容放入 child 属性中,那么在触发 builder 时,会直接使用这个 child,不会再构建一遍 child
总结:ValueListenableBuilder中如果child 有,则builder中不会再次创建这个child,child只会生成一次,可以减少刷新,这也就是child的由来

5.适用场景

监听ScrollVIew等滑动Widget,当滑动时,可以监听它们滑动的位置,然后通过ValueListenableBuilder进行局部的刷新,这样效率更多,性能更高

案例:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return MaterialApp(
        home: Scaffold(
      body: Example508(),
    ));
  }
}

class Example508 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ExampleState();
  }
}

class _ExampleState extends State<Example508> {
  var imgList = [
    'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2877516247,37083492&fm=26&gp=0.jpg',
    'https://www.itying.com/images/flutter/1.png'
  ];

  // 页数监听对象
  final ValueNotifier<int> page = ValueNotifier<int>(1);

  // 进度监听对象
  final ValueNotifier<double> factor = ValueNotifier<double>(1 / 2);

  /// 初始化控制器
  PageController pageController;

  //PageView当前显示页面索引
  int currentPage = 0;

  Color get startColor => Colors.red; // 起点颜色
  Color get endColor => Colors.blue; // 终点颜色

  @override
  void initState() {
    super.initState();
    //创建控制器的实例
    pageController = new PageController(
      viewportFraction: 0.9,
      //用来配置PageView中默认显示的页面 从0开始
      initialPage: 0,
      //为true是保持加载的每个页面的状态
      keepPage: true,
    );

    ///PageView设置滑动监听
    pageController.addListener(() {
      // //PageView滑动的距离
      // double offset = pageController.offset;
      // //当前显示的页面的索引
      // double page = pageController.page;
      // print("pageView 滑动的距离 $offset  索引 $page");
      double value =
          (pageController.page + 1) % imgList.length / imgList.length;
      factor.value = value == 0 ? 1 : value;
      print("pageView 滑动的距离 ${factor.value}");
    });
  }

  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,
          ),
        ],
      ),
    );
  }

  Widget _buildWithPageChange(BuildContext context, int value, Widget child) {
    return Text(
      "绘制集录 ${value / imgList.length}",
      style: TextStyle(fontSize: 30, color: Colors.white),
    );
  }

  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(
                Colors.red,
              ),
            );
          },
        ),
      );

  Widget _buildPageView() {
    return Container(
      height: 200,
      child: PageView.builder(
        //当页面选中后回调此方法
        //参数[index]是当前滑动到的页面角标索引 从0开始
        onPageChanged: (int index) {
          page.value = index + 1;
        },
        //值为flase时 显示第一个页面 然后从左向右开始滑动
        //值为true时 显示最后一个页面 然后从右向左开始滑动
        reverse: false,
        //滑动到页面底部无回弹效果
        physics: BouncingScrollPhysics(),
        //纵向滑动切换
        scrollDirection: Axis.horizontal,
        //页面控制器
        controller: pageController,
        //所有的子Widget
        itemBuilder: (BuildContext context, int index) {
          return Container(
            height: 200,
            //缩放的图片
            width: MediaQuery.of(context).size.width,
            child: Image.network(
              imgList[index % 2],
            ),
          );
        },
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: ValueListenableBuilder(
      valueListenable: factor,
      builder: (_, value, child) => Container(
        height: 500,
        color: Color.lerp(startColor, endColor, value),
        child: child, //<--- tag1
      ),
      child: Column(
        children: [
          _buildTitle(context),
          _buildPageView(),
          SizedBox(
            height: 20,
          ),
          _buildProgress(),
        ],
      ),
    ));
  }
}

备注:PageView.builder一定要有高度,一般设置在Container中,否则会报错,hasSize

颜色可以通过 Color.lerp 来计算两个颜色之间对应分度值的颜色,这样就实现了,背景动态换肤