RiverPod 2.0 Family Provider状态重置而非更新问题排查
问题分析与解决方案
问题根源
Provider状态被重复重置
你的EditTopicController的build方法中,每次ref.voices.watchAll或ref.topics.watchOne的数据更新时,都会重新执行build,而你在build里直接从topic重新生成selectedIds并赋值给新的EditTopicControllerState,这会覆盖掉通过addVoice手动添加的ID,导致新增的语音ID无法保留。页面重复创建控制器
在_EditTopicScreenState的build方法中每次都重新创建TextEditingController,不仅浪费性能,还可能丢失输入框的状态。
修复步骤
1. 修改EditTopicController,避免状态被覆盖
将selectedIds的初始化逻辑改为仅在首次构建时执行,后续依赖手动方法维护状态,同时通过ref.listen监听topic的变化(可选,根据业务需求决定是否同步topic的voices)。
修改后的控制器代码:
@riverpod class EditTopicController extends _$EditTopicController { @override EditTopicControllerState build({required String topicId}) { // 可选:当topic数据更新时,同步selectedIds(根据业务需求开启) ref.listen( topicsWatchOneProvider(topicId: topicId, remote: false), (previous, next) { if (next.model != null) { final newSelectedIds = next.model!.voices.map((e) => e.id!).toList(); state = state.copyWith(selectedIds: newSelectedIds); } }, ); final voices = ref.voices.watchAll(remote: false).model; final activeVoices = voices?.where((v) => v.archivedAt == null).toList() ?? []; final topic = ref.topics.watchOne(topicId, remote: false).model; // 仅首次初始化时使用topic的voices生成selectedIds final initialSelectedIds = topic?.voices.map((e) => e.id!).toList() ?? []; final List<Voice> sortedVoices = [ ...activeVoices.where((v) => initialSelectedIds.contains(v.id)).toList(), ...activeVoices.where((v) => !initialSelectedIds.contains(v.id)).toList() ]; return EditTopicControllerState( name: topic?.name ?? '', selectedIds: initialSelectedIds, sortedVoices: sortedVoices, ); } List<String> get selectedIds => state.selectedIds; String get name => state.name; void updateTopicName(String name) { state = state.copyWith(name: name); } void addVoice(String id) { if (!state.selectedIds.contains(id)) { state = state.copyWith(selectedIds: [...state.selectedIds, id]); } } void removeVoice(String id) { state = state.copyWith( selectedIds: state.selectedIds.where((v) => v != id).toList(), ); } void createVoice(String name) async { final voice = await Voice.newVoice( name: name, color: HexColorParser().generateRandomHexColor(), ).save(remote: true); addVoice(voice.id!); } }
2. 修复页面中的TextEditingController问题
将TextEditingController移到initState中初始化,避免每次build重建:
class _EditTopicScreenState extends ConsumerState<EditTopicScreen> { late TextEditingController topicNameFieldController; @override void initState() { super.initState(); // 首次初始化时获取控制器的name值 final initialName = ref.read(editTopicControllerProvider(topicId: widget.topicId).notifier).name; topicNameFieldController = TextEditingController(text: initialName); } @override void dispose() { topicNameFieldController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final controller = ref.read(editTopicControllerProvider(topicId: widget.topicId).notifier); final state = ref.watch(editTopicControllerProvider(topicId: widget.topicId)); return FlutterDataWidget<Topic?>( findRequest: ref.topics.findOne(widget.topicId, remote: false), child: (topic) { return Drawer( // ... 原有代码保持不变 body: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ // ... VoiceCreatorV2( onAdd: (name) => controller.createVoice(name), ), const SizedBox(height: 20), Flexible( child: VoicesSelectV2( selectedVoiceIds: state.selectedIds, onAdd: (voiceId) => controller.addVoice(voiceId), onRemove: (voiceId) => controller.removeVoice(voiceId), ), ), // ... ], ), ), ); }, ); } }
关键概念补充
RiverPod中,@riverpod生成的类的build方法是响应式触发的:当它内部监听的Provider(比如ref.voices.watchAll)数据变化时,build会重新执行并返回新的state。如果你的state修改逻辑依赖build里的外部数据,就会导致手动修改的state被覆盖。正确的做法是:
build方法仅负责首次初始化或依赖数据变化时的基础状态设置- 手动修改state的操作(如
addVoice)通过state.copyWith维护,避免被build方法的重新执行覆盖
内容的提问来源于stack exchange,提问作者Chris King
相关产品推荐
相关产品推荐

