无需外部状态管理库,孙子组件能否触发祖父组件状态更新?
问题:孙子组件触发祖父组件状态更新的解决方案
我现在找不到合适的方法让孙子组件触发祖父组件的状态更新。我的应用所有数据都存在本地设备数据库里,读写都走这个库。
我一直不想用状态管理库,因为这个应用不算复杂,引入这类库有点杀鸡用牛刀。
页面结构是:祖父组件是ListView,子组件是自定义的ListTile。每个ListTile上有两个图标按钮,分别对应编辑和删除,点击后会弹出不同的AlertDialog(也就是孙子组件)。当我执行更新或删除操作时,数据会写入数据库并返回一个Future,这时候需要祖父组件ListView刷新状态重新拉取数据。StatefulBuilder只能刷新孙子组件的状态,没法跨层级触发更新。
想问下这种情况是必须引入BLOC或Riverpod这类状态管理方案,还是有其他可行的办法?
ListView 祖父组件代码
@override Widget build(BuildContext context) { return Scaffold( body: Builder( builder: (BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ // other children here Expanded( flex: 11, child: FutureBuilder<List<MyCustomObject>>( future: _getQuotes(), // queries the db builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting && !snapshot.hasData) { return const Center( child: SizedBox( height: AppDims.smallSizedBoxLoadingProgress, width: AppDims.smallSizedBoxLoadingProgress, child: CircularProgressIndicator() ), ); } else if (snapshot.hasError) { log(snapshot.error.toString()); log(snapshot.stackTrace.toString()); return Center(child: Text(snapshot.error.toString())); } else { // no point using StatefulBuilder here, as i need // to potentially trigger _getQuotes() again to rebuild the entire ListView return ListView.builder( padding: const EdgeInsets.symmetric( horizontal: AppDims.textHorizontalPadding, vertical: AppDims.textVerticalPadding ), itemCount: snapshot.data!.length, itemBuilder: (context, int index) { return MyCustomTile( // tile data mapping from snapshot for MyCustomObject ); }, ); } }, ) ) ]; }, ), ); }
MyCustomTile 子组件代码
@override Widget build(BuildContext context) { return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDims.tileBorderRadius), side: const BorderSide( color: Colors.green, width: 1.5, ) ), child: ListTile( // other omitted ListTile params here trailing: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.edit), onPressed: () => showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return EditDialog(); } ).then((_) => setState(() {})), // will only setState on the dialog! ), IconButton( icon: const Icon(Icons.delete), onPressed: () => showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) => DeleteWarningDialog( widget.id, AppStrings.price.toLowerCase(), true ), ), ), ] ), ), ); }
DeleteWarningDialog 孙子组件代码
@override Widget build(BuildContext context) { return AlertDialog( title: Text(_buildFinalWarningString()), actions: [ TextButton( child: const Text(AppStrings.cancel), onPressed: () => Navigator.pop(context), ), TextButton( child: const Text(AppStrings.delete), onPressed: () { _appDatabase.deleteFoo(widget.objectIdToDelete); Navigator.pop(context); }, ) ], ); }
解决方案
不需要立刻引入状态管理库,以下两种轻量方案就能解决问题:
方案1:回调函数传递刷新逻辑
通过将祖父组件的刷新方法层层传递给子组件、孙子组件,在数据操作完成后触发刷新:
- 祖父组件添加刷新方法
在ListView对应的State类中新增触发列表刷新的方法:
void _refreshList() { setState(() {}); // 触发FutureBuilder重新执行_getQuotes()查询数据库 }
- 将刷新方法传递给子组件
创建MyCustomTile时,把_refreshList作为参数传入:
return MyCustomTile( // 原有数据参数 onDataChanged: _refreshList, );
- 子组件接收并调用回调
修改MyCustomTile的参数定义,在Dialog关闭后调用刷新方法:
class MyCustomTile extends StatelessWidget { final VoidCallback onDataChanged; // 其他参数 const MyCustomTile({super.key, required this.onDataChanged, /* 其他参数 */}); @override Widget build(BuildContext context) { // ... 原有代码 IconButton( icon: const Icon(Icons.edit), onPressed: () => showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) => EditDialog(), ).then((_) => onDataChanged()), // 触发祖父组件刷新 ), IconButton( icon: const Icon(Icons.delete), onPressed: () => showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) => DeleteWarningDialog( widget.id, AppStrings.price.toLowerCase(), true ), ).then((_) => onDataChanged()), // 删除后触发刷新 ), // ... 原有代码 } }
- 确保数据操作完成后关闭弹窗
修改DeleteWarningDialog的删除逻辑,等待数据库操作完成再关闭弹窗:
TextButton( child: const Text(AppStrings.delete), onPressed: () async { await _appDatabase.deleteFoo(widget.objectIdToDelete); // 等待删除完成 Navigator.pop(context); }, )
方案2:用ValueNotifier实现轻量状态通知
如果组件层级较多,回调嵌套显得繁琐,可以用Flutter自带的ValueNotifier做跨组件状态通知:
- 祖父组件创建
ValueNotifier
在State类中初始化一个用于触发刷新的Notifier:
final ValueNotifier<bool> _listRefreshTrigger = ValueNotifier(false);
- 用
ValueListenableBuilder包裹FutureBuilder
让列表监听Notifier的变化,触发重新查询:
Expanded( flex: 11, child: ValueListenableBuilder( valueListenable: _listRefreshTrigger, builder: (context, _, child) { return FutureBuilder<List<MyCustomObject>>( future: _getQuotes(), builder: (context, snapshot) { // 原有builder逻辑不变 }, ); }, ), )
- 添加刷新触发方法
void _refreshList() { _listRefreshTrigger.value = !_listRefreshTrigger.value; // 切换值触发监听回调 }
后续传递_refreshList的方式和方案1一致即可。
是否需要引入状态管理库?
如果你的应用只有这一处跨组件状态更新需求,上面两种方案完全够用,没必要引入BLOC或Riverpod。但如果后续功能扩展,出现更多跨页面、跨组件的状态同步场景,再考虑引入状态管理库会更合理。
内容的提问来源于stack exchange,提问作者tommytucker7182
相关产品推荐
相关产品推荐

