关于Flutter中ValueNotifier的触发机制

9,596 阅读4分钟

最近在项目中使用到了ValueNotifier这个方式去触发界面的刷新,替代了使用setState(){}的使用,因为我们知道在使用setState(){}时,会导致整个界面的重新build,而往往我们想更新的widget只是某一个或几个,所以整个界面的重绘,会造成不必要的性能损耗,当然再界面元素少的时候看不出差异,但设想一下,某个界面有1000个widget,但我们只选改变状态的只是其中一个widget,当我们使用setState(){}时,就会造成不必要的999个widget的绘制,所以基于这些问题,今天我们就研究一下ValueNotifier的使用方式和注意事项

例:先来解决一个简单的需求,比如我的界面中心有个显示数字的文案,当通过按钮改变数字时,需要立即刷新数字文案的改变.

先来看一下简单的界面效果:

当点击加号按钮是改变数字,并且及时刷新界面,传统使用setState(){}的写法:

import 'package:flutter/material.dart';
class NumberChangeDemo extends StatefulWidget {
  @override
  _NumberChangeDemoState createState() => _NumberChangeDemoState();
}

class _NumberChangeDemoState extends State<NumberChangeDemo> {

  int number = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Center(
        child: Text("当前的数字:$number",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
      ),

      floatingActionButton: FloatingActionButton(
        child: Text("+",style: TextStyle(fontSize: 20),),
        onPressed: (){
          // 数字改变
          number ++;
          // 刷新界面
          setState(() {
          });
        },
      ),
    );
  }
}

使用ValueNotifier改变后的代码,实现效果一样:

import 'package:flutter/material.dart';
class NumberChangeDemo extends StatefulWidget {
  @override
  _NumberChangeDemoState createState() => _NumberChangeDemoState();
}

class _NumberChangeDemoState extends State<NumberChangeDemo> {

  int number = 0;
  
  // 创建一个监听实例,在需要监听这个改变的地方使用ValueListenableBuilder去包裹你的Widget
  ValueNotifier<int> numberNotifier = new ValueNotifier(0);

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    numberNotifier.value = number;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: ValueListenableBuilder(
        valueListenable: numberNotifier,
        // value前面的int代表值的类型,使用时一定明确指定该类型
        builder: (BuildContext context, int value, Widget child) {
          return Center(
            child: Text("当前的数字:$value",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          );
        },
      ),

      floatingActionButton: FloatingActionButton(
        child: Text("+",style: TextStyle(fontSize: 20),),
        onPressed: (){
          // 数字改变
          number ++;
          // 只刷新监听了numberNotifier的界面
          numberNotifier.value = number;
        },
      ),
    );
  }
}

可以看到达到的效果是一样的,但是ValueNotifier只是对监听的地方做了一个脏标记,然后在下次信号来的时候,才会去对打了脏标记的widget做一个重绘工作,减少了其他8个不必要的Widget的绘制,而使用了setState(){}后会对所有的Widget打脏标记,然后再绘制,优势显而易见!

上面是对基本数据类型: int的使用,bool, String类型同理.

这里再说说使用ValueNotifier遇到的一些坑,当ValueNotifier监听的对象是一个数据模型时,比如监听下面的这个数据模型:

import 'package:flutter/material.dart';

// 要监听的数据模型
class DemoModel {
  int number;

  DemoModel({this.number});

  DemoModel.fromJson(Map<String, dynamic> json) {
    number = json['number'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['number'] = this.number;
    return data;
  }
}

// 界面
class NumberChangeDemo extends StatefulWidget {
  @override
  _NumberChangeDemoState createState() => _NumberChangeDemoState();
}

class _NumberChangeDemoState extends State<NumberChangeDemo> {

  DemoModel demoModel = DemoModel(number: 0);

  // 创建一个监听实例,在需要监听这个改变的地方使用ValueListenableBuilder去包裹你的Widget
  ValueNotifier<DemoModel> numberNotifier = new ValueNotifier(DemoModel(number: 0));

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    numberNotifier.value = demoModel;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Column(
        children: <Widget>[

          Text("不需要改变的Widget1",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          Text("不需要改变的Widget2",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          Text("不需要改变的Widget3",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          Text("不需要改变的Widget4",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          Text("不需要改变的Widget5",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          Text("不需要改变的Widget6",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          Text("不需要改变的Widget7",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          Text("不需要改变的Widget8",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
          ValueListenableBuilder(
            valueListenable: numberNotifier,

            // value前面的int代表值的类型,使用时一定明确指定该类型
            builder: (BuildContext context, DemoModel value, Widget child) {
              return Center(
                child: Text("需要改变的Widget的数字:${value.number}",style: TextStyle(color: Colors.red,fontSize: 20,fontWeight: FontWeight.bold),),
              );
            },
          ),
        ],
      ),

      floatingActionButton: FloatingActionButton(
        child: Text("+",style: TextStyle(fontSize: 20),),
        onPressed: (){
          // 数据模型中的某个值改变
          demoModel.number ++;
          // 只刷新监听了numberNotifier的界面
          numberNotifier.value = demoModel;
        },
      ),
    );
  }
}

你会发现点击加号,虽然 demoModel.number 的值改变了,并且也通过 numberNotifier.value = demoModel;赋值了监听的数据模型,但是真正界面显示时,数字并没有改变.这就很奇怪,和预想的有差别,通过研究发现,在监听对象时,单存改变对象的某一个属性值,是不会触发 ValueListenableBuilder 的构建的,因为实际上对象的内存地址没有改变,所以机制上判定为不触犯监听,所以当监听对象时,我们需要的是监听的对象本身,需要变成一个新对象,改造后的代码如下:

单存展示需要注意的地方的代码:

// 数据模型中的某个值改变
   demoModel.number ++;
// **************需要改变监听的对象,可以通过创建一个新对象的方式来出发监听*****************
   DemoModel newDemoModel = DemoModel(number: demoModel.number);
   numberNotifier.value = newDemoModel;

发现这样修改后的代码可以正常的出发界面的刷新了

不急不躁,好好学习!!