如何通过FutureProvider正确更新Freezed状态?报错求助
错误信息
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Tried to modify a provider while the widget tree was building.
If you are encountering this error, chances are you tried to modify a provider
in a widget life-cycle, such as but not limited to:
- build
- initState
- dispose
- didUpdateWidget
- didChangeDepedencies
Modifying a provider inside those life-cycles is not allowed, as it could
lead to an inconsistent UI state. For example, two widgets could listen to the
same provider, but incorrectly receive different states.To fix this problem, you have one of two solutions:
(preferred) Move the logic for modifying your provider outside of a widget
life-cycle. For example, maybe you could update your provider inside a button's
onPressed instead.Delay your modification, such as by encasuplating the modification
in aFuture(() {...}).
This will perform your update after the widget tree is done building.
相关代码
状态类
@freezed class DevicePageState<T> with _$DevicePageState { const factory DevicePageState.initial() = Initial; const factory DevicePageState.loading() = Loading; const factory DevicePageState.loaded(AsyncValue<List<Device>> devices) = Loaded; const factory DevicePageState.error() = Error; }
StateNotifier抽象类
abstract class IRepository extends StateNotifier<DevicePageState> { IRepository(DevicePageState devicePageState) : super(const DevicePageState.initial()); Future<void> fetchDevices(); Future<void> fetchTargetDevice(); Future<void> updateDevice(); }
Repository实现
class DeviceApiRepository extends IRepository { final UPnPDiscoveryService upnpDiscoveryService; final Ref ref; DeviceApiRepository({required this.ref, required this.upnpDiscoveryService}) : super(const DevicePageState.loading()); @override Future<void> fetchDevices() async { try { // state = const DevicePageState.loading(); final devices = ref.watch(upnpDevicesProviderProvider); state = DevicePageState.loaded(devices); } catch (e) { state = const DevicePageState.error(); } } @override Future<void> fetchTargetDevice() async { try { state = const DevicePageState.loading(); final targetDevice = ref.watch(upnpDevicesProviderProvider); state = DevicePageState.loaded(targetDevice); } catch (e) { state = const DevicePageState.error(); } } @override Future<void> updateDevice() async { try { state = const DevicePageState.loading(); final devices = ref.watch(upnpDevicesProviderProvider); state = DevicePageState.loaded(devices); } catch (e) { state = const DevicePageState.error(); } } }
Widget调用代码
@override Widget build(BuildContext context) { ref.read(deviceApiRepositoryProvider.notifier).fetchDevices(); final data = ref.watch(deviceApiRepositoryProvider); // 构建UI... }
解决方案
核心问题
错误根源是在build方法中调用fetchDevices,该方法会修改StateNotifier的state,而Riverpod禁止在Widget构建生命周期内修改Provider状态,避免UI不一致。
方案一:响应式自动监听依赖(推荐)
让Repository自动监听upnpDevicesProviderProvider的变化,无需手动调用fetchDevices,符合Riverpod的响应式设计:
修改DeviceApiRepository的实现:
class DeviceApiRepository extends IRepository { final UPnPDiscoveryService upnpDiscoveryService; final Ref ref; DeviceApiRepository({required this.ref, required this.upnpDiscoveryService}) : super(const DevicePageState.loading()) { // 监听设备数据变化,自动更新状态 ref.listen<AsyncValue<List<Device>>>(upnpDevicesProviderProvider, (previous, next) { state = DevicePageState.loaded(next); }); // 初始化时触发一次加载 _initFetch(); } Future<void> _initFetch() async { try { state = const DevicePageState.loading(); final devices = ref.read(upnpDevicesProviderProvider); state = DevicePageState.loaded(devices); } catch (e) { state = const DevicePageState.error(); } } // 保留其他方法的实现... }
然后修改Widget代码,仅监听状态即可:
@override Widget build(BuildContext context) { final data = ref.watch(deviceApiRepositoryProvider); // 根据data的状态构建UI(loading/loaded/error) }
方案二:延迟调用(应急方案)
如果必须在Widget构建时触发数据获取,使用PostFrameCallback延迟到Widget树构建完成后执行:
@override Widget build(BuildContext context) { WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(deviceApiRepositoryProvider.notifier).fetchDevices(); }); final data = ref.watch(deviceApiRepositoryProvider); // 构建UI... }
注意:此方案会在每次Widget重建时触发调用,建议添加判断条件避免重复执行,比如仅在状态为Initial时调用。
内容的提问来源于stack exchange,提问作者RootKid87

