Flutter ValueNotifier使用以及ValueListenableBuilder中child为什么能避免被rebuild

923 阅读4分钟

ValueNotifier介绍

flutter自带的一个监听对象,可以不需要使用setState来让数据更新UI。

例如:

ValueNotifier<int> count = ValueNotifier<int>(0);

GetstureDetector(
	onTap:(){
		count.value++;
	}
	child:Text(count.toString())
)

仔细查看之后发现,ValueNotifier其实是继承了ChangeNotifier(我说咋名字那么像)。

下面是ValueNotifier的源码:

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  /// Creates a [ChangeNotifier] that wraps this value.
  ValueNotifier(this._value);

  /// The current value stored in this notifier.
  ///
  /// When the value is replaced with something that is not equal to the old
  /// value as evaluated by the equality operator ==, this class notifies its
  /// listeners.
  @override
  T get value => _value;
  T _value;
  set value(T newValue) {
    if (_value == newValue)
      return;
    _value = newValue;
    notifyListeners();
  }

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

可以看到,在set value之后,会调用notifyListeners来通知所有的监听者。然后我们再看下搭配ValueNotifier使用的ValueListenableBuilder。先看源码:

class ValueListenableBuilder<T> extends StatefulWidget {
  /// Creates a [ValueListenableBuilder].
  ///
  /// The [valueListenable] and [builder] arguments must not be null.
  /// The [child] is optional but is good practice to use if part of the widget
  /// subtree does not depend on the value of the [valueListenable].
  const ValueListenableBuilder({
    Key? key,
    required this.valueListenable,
    required this.builder,
    this.child,
  }) : assert(valueListenable != null),
       assert(builder != null),
       super(key: key);

  /// The [ValueListenable] whose value you depend on in order to build.
  ///
  /// This widget does not ensure that the [ValueListenable]'s value is not
  /// null, therefore your [builder] may need to handle null values.
  ///
  /// This [ValueListenable] itself must not be null.
  final ValueListenable<T> valueListenable;

  /// A [ValueWidgetBuilder] which builds a widget depending on the
  /// [valueListenable]'s value.
  ///
  /// Can incorporate a [valueListenable] value-independent widget subtree
  /// from the [child] parameter into the returned widget tree.
  ///
  /// Must not be null.
  final ValueWidgetBuilder<T> builder;

  /// A [valueListenable]-independent widget which is passed back to the [builder].
  ///
  /// This argument is optional and can be null if the entire widget subtree
  /// the [builder] builds depends on the value of the [valueListenable]. For
  /// example, if the [valueListenable] is a [String] and the [builder] simply
  /// returns a [Text] widget with the [String] value.
  final Widget? child;

  @override
  State<StatefulWidget> createState() => _ValueListenableBuilderState<T>();
}

class _ValueListenableBuilderState<T> extends State<ValueListenableBuilder<T>> {
  late T value;

  @override
  void initState() {
    super.initState();
    value = widget.valueListenable.value;
    widget.valueListenable.addListener(_valueChanged);
  }

  @override
  void didUpdateWidget(ValueListenableBuilder<T> 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);
  }
}

首先在initState方法里面,给valueListenable加了一个监听回调,每次更新之后调用_valueChanged,在_valueChanged里面,就是常见的setState了,没啥好说的。接着就是didUpdateWidgetdispose里面的更改及释放监听了。

ValueListenableBuilder初始化参数里面,有个child,可以将不需要随着valueListenable更改的组件放到child里面,可以提升效率。因为在ValueListenableBuilder执行setState的时候,child是不会更改的。

为什么child没有rebuild呢?

demo代码:

class NewHomePage extends StatefulWidget {
  NewHomePage({Key? key}) : super(key: key);

  _NewHomePageState createState() => _NewHomePageState();
}

class _NewHomePageState extends State<NewHomePage> {
  ValueNotifier<int> valueNotifier = ValueNotifier<int>(0);

  Widget build(BuildContext context) {
    return Scaffold(
        body: ValueListenableBuilder<int>(
          valueListenable: valueNotifier,
          child: ColorBox(
            color: Colors.blue,
          ),
          builder: (context, count, child) {
            return Column(
              children: [
                Text(
                  count.toString(),
                ),
                child!,
              ],
            );
          },
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            valueNotifier.value++;
          },
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ));
  }
}

class ColorBox extends StatefulWidget {
  const ColorBox({required this.color});
  final Color color;

  @override
  State<ColorBox> createState() => _ColorBoxState();
}

class _ColorBoxState extends State<ColorBox> {
  int count = 0;

  @override
  void didUpdateWidget(covariant ColorBox oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return GestureDetector(
      onTap: () {
        count++;
        setState(() {});
      },
      child: Container(
        color: widget.color,
        width: 100,
        height: 100,
        child: Center(
          child: Text(
            count.toString(),
            style: TextStyle(fontSize: 30),
          ),
        ),
      ),
    );
  }
}

运行这个例子之后发现,不管怎么点击Icons.add,ColorBox都没有rebuild,确实符合ValueListenableBuilder的预期实现。

原因分析:

首先我们需要知道setState的原理(旧文章指路)。简单描述就是,从被标脏的结点开始,更新自己,并且往下「尝试」对每一个子结点,重复这个动作。本次讨论的重点,就出现在「尝试」这两个字。

Element类updateChild方法部分注释:

  ///class Element
  /// |                     | **newWidget == null**  | **newWidget != null**   |
  /// | :-----------------: | :--------------------- | :---------------------- |
  /// |  **child == null**  |  Returns null.         |  Returns new [Element]. |
  /// |  **child != null**  |  Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
  ///

可以看到,在Element.child不为空,newWidget不为空的情况下,是Old child updated if possible, returns child or new [Element].而不是一定会更新。

Element类updateChild方法源码:

Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    final Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        newChild = child;
      } else {
        deactivateChild(child);
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      newChild = inflateWidget(newWidget, newSlot);
    }
    return newChild;
  }

ValueListenableBuilder在尝试更新child的时候,执行if (hasSameSuperclass && child.widget == newWidget)时,child.widgetnewWidget都是引用了同一个对象,即ValueListenableBuilder.child所以child实际上没有更改。即达到了ValueListenableBuilder不更新child的效果。

如果将demo改成:

ValueListenableBuilder<int>(
          valueListenable: valueNotifier,
          child: ColorBox(
            color: Colors.blue,
          ),
          builder: (context, count, child) {
            return Column(
              children: [
                Text(
                  count.toString(),
                ),
                ColorBox(
                  color: Colors.blue,
                )
              ],
            );
          },
        )

不使用child,但是创建一个数据和child完全一样的新对象,那么,builder里面的ColorBox,在ValueListenableBuilder执行setState的时候,一样会rebuild。因为在执行if (hasSameSuperclass && child.widget == newWidget)时,是两个数据一样(color一样,State里面的count一样)的对象,但并不是指向同一个地址的引用(hashCode不同,引用不同),所以不会往下走,而是进到else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)),往后会执行到ColorBox的rebuild

总结一下

ValueListenableBuilder可以实现局部刷新,ValueListenableBuilder.child能避免被rebuild,是因为比较前后,都是同一个对象的引用。

如果有写的不对的地方,感谢指正。