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了,没啥好说的。接着就是didUpdateWidget和dispose里面的更改及释放监听了。
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.widget和newWidget都是引用了同一个对象,即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,是因为比较前后,都是同一个对象的引用。
如果有写的不对的地方,感谢指正。