Flutter更新Notifier值后ValueListenableBuilder builder未调用
问题现象
从父Widget更新notifier值时,定义在子Widget中的ValueListenableBuilder在值变更后未触发builder方法调用。触发更新时DealsNotifier类内部的打印日志可正常输出,证明值修改逻辑已执行,但builder回调始终不执行。
相关代码实现
父Widget代码
父Widget中声明了两个StatefulWidget类型的子组件,同时声明了Notifier类的静态实例。在secondChild组件中调用updateMenuItemList方法更新firstChild组件的列表数据:
class HotKeysWidget extends StatefulWidget { static HotKeysWidgetState? of(BuildContext context) => context.findAncestorStateOfType<HotKeysWidgetState>(); @override State<StatefulWidget> createState() { return HotKeysWidgetState(); } } class HotKeysWidgetState extends State<HotKeysWidget> { static DealsNotifier appValueNotifier = DealsNotifier(); updateMenuItemList(List<Food> list) { appValueNotifier.updateMenuList(list); } @override Widget build(BuildContext context) { return Container(child: Column(children: [ firstChild(), secondChild(), ], ), ); } }
Notifier类代码
class DealsNotifier { ValueNotifier<List<Food>> dealList = ValueNotifier([]); ValueNotifier<List<Food>> menuitemList = ValueNotifier([]); ValueNotifier<List<Map<String,dynamic>>> categoryList = ValueNotifier([]); void updateDealsList(List<Food> list) { dealList.value = list; print('DEAL LIST IN CLASS: ${dealList}'); } void updateMenuList(List<Food> list) { menuitemList.value = list; print('PRICE CHANGE: ${menuitemList.value[2].price}'); print('MENU ITEM LIST IN CLASS: ${menuitemList}'); } void updateCategoryList(List<Map<String,dynamic>> catList) { categoryList.value = catList; print('DEAL LIST IN CLASS: ${categoryList}'); } List<Food> getDealList() { return dealList.value; } List<Food> getMenuitemList() { return menuitemList.value; } List<Map<String,dynamic>> getCategoryList() { return categoryList.value; } }
firstChild组件代码
ValueListenableBuilder定义在该组件中:
class firstChild extends StatefulWidget { @override State<StatefulWidget> createState() { return firstChildState(); } } class firstChildState extends State<firstChild> { @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: HotKeysWidgetState.appValueNotifier.menuitemList, builder: (context, List<Food> value, widget) { print('MENUITEM LIST UPDATED: ${value}'); return HotkeysMenuItemsWidget( key: menuItemsKey, currentMenu:currentCat != null ? value : [], ); }, ); } }
secondChild组件代码
class secondChild extends StatefulWidget { @override State<StatefulWidget> createState() { return secondChildState(); } } class secondChildState extends State<secondChild> { @override Widget build(BuildContext context) { return RaisedButton( onPressed: (){ HotKeysWidget.of(context)!.updateMenuItemList([]); }, child: Text( 'UPDATE', maxLines: 2, textAlign: TextAlign.center, style: const TextStyle( fontSize: 12, ), ), ); } }
问题原因
核心原因是ValueNotifier的内置更新判断逻辑:ValueNotifier的value setter默认会通过==对比新旧值,如果判断结果为相等,会直接终止流程,不调用notifyListeners(),也就不会触发ValueListenableBuilder的重建。
// ValueNotifier 内置value set逻辑 set value(T newValue) { if (_value == newValue) return; _value = newValue; notifyListeners(); }
对于List这类引用类型,==判断的是两个对象的内存地址是否一致,而非列表内容是否相等。如果更新时传入的List<Food>实例和menuitemList当前持有的value是同一个对象(比如直接在原有列表对象上做增删改后传入原对象),哪怕列表内容已经变化,==判断依然会返回true,最终不会触发重建。
另外可排查次要原因:如果menuitemList实例在某处被调用过dispose()方法,后续修改value也不会触发通知。
解决方案
方案1(推荐):更新列表时始终传入新实例
修改updateMenuList方法,传入列表时创建新的列表副本,保证和旧值引用不同:
void updateMenuList(List<Food> list) { // 通过List.from或者展开运算符创建新的列表实例 menuitemList.value = List<Food>.from(list); // 打印建议取value,不要直接打印ValueNotifier实例,访问下标前先判断长度避免越界 if(menuitemList.value.length >=3) print('PRICE CHANGE: ${menuitemList.value[2].price}'); print('MENU ITEM LIST IN CLASS: ${menuitemList.value}'); }
日常修改列表时不要直接操作原value的内容,比如不要写menuitemList.value.add(xxx),应该基于旧列表创建新列表后再赋值:
// 错误写法:直接修改原列表内容,不会触发更新 // final oldList = HotKeysWidgetState.appValueNotifier.menuitemList.value; // oldList.add(newFood); // HotKeysWidgetState.appValueNotifier.updateMenuList(oldList); // 正确写法:创建新列表传入 final oldList = HotKeysWidgetState.appValueNotifier.menuitemList.value; HotKeysWidgetState.appValueNotifier.updateMenuList([...oldList, newFood]);
方案2:自定义列表专用ValueNotifier
如果业务场景需要在同引用内容变更时也触发更新,可以自定义ValueNotifier跳过相等判断:
class ListValueNotifier<T> extends ValueNotifier<List<T>> { ListValueNotifier(super.initialValue); @override set value(List<T> newValue) { _value = newValue; notifyListeners(); } }
将DealsNotifier中的列表类型替换为该自定义类即可。
额外优化建议
- 类名遵循Dart大驼峰命名规范,
firstChild/secondChild建议修改为FirstChild/SecondChild - 全局共享的Notifier不要存放在Widget的State中作为静态属性,建议使用Provider、GetIt等成熟的状态管理/依赖注入方案托管,避免生命周期混乱
- 访问列表下标前先判断长度,避免打印日志或业务逻辑中触发越界错误
内容的提问来源于stack exchange,提问作者Dev94

