You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RiverPod 2.0 Family Provider状态重置而非更新问题排查

问题分析与解决方案

问题根源

  1. Provider状态被重复重置
    你的EditTopicController的build方法中,每次ref.voices.watchAll或ref.topics.watchOne的数据更新时,都会重新执行build,而你在build里直接从topic重新生成selectedIds并赋值给新的EditTopicControllerState,这会覆盖掉通过addVoice手动添加的ID,导致新增的语音ID无法保留。

  2. 页面重复创建控制器
    在_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 16:05:42