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

Riverpod 2.6.1中提供者构建方法重复执行(引发重复API请求)及编辑时状态值无法保存的问题求助

Riverpod 2.6.1中提供者构建方法重复执行(引发重复API请求)及编辑时状态值无法保存的问题求助

大家好,我现在用Riverpod 2.6.1配合代码生成(@riverpod注解)开发Flutter应用,遇到两个棘手的问题,想请教下各位:

第一个问题是Provider的build方法在App启动时会执行两次,直接导致重复的网络请求,哪怕是最基础的Provider设置也会出现这个情况。我在applyForMembershipControllerProvider的build逻辑里做了API调用,每次打开ApplyForMembershipScreen都会触发两次请求,这显然不符合预期。

第二个问题是编辑表单时状态值没法正确同步保存。比如在MembershipDetailsForm里,我用TextEditingController绑定输入框,但每次更新表单数据后,控制器的文本没法及时同步;我尝试在didChangeDependencies里初始化控制器,还加了ref.listen监听,但逻辑绕来绕去,有时候还是不生效——比如修改输入后切换步骤再切回来,输入框的内容又回到旧值了。

相关代码片段

ApplyForMembershipScreen 核心代码

class ApplyForMembershipScreen extends ConsumerStatefulWidget with AppBaseUtils {
  final String serviceId;
  final String? applicationId;

  ApplyForMembershipScreen({super.key, required this.serviceId, this.applicationId});

  @override
  ConsumerState<ApplyForMembershipScreen> createState() => _ApplyForMembershipScreenState();
}

class _ApplyForMembershipScreenState extends ConsumerState<ApplyForMembershipScreen> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    final membershipFormKey = GlobalKey<FormState>();
    final stepperList = ['Membership Details', 'Certificate Details', 'Attachments'];
    final provider = applyForMembershipControllerProvider(applicationId: widget.applicationId);
    final asyncState = ref.watch(provider);
    final controller = ref.read(provider.notifier);

    return AppScaffold(
      dismissKeyboardOnTap: true,
      hideWhiteBackground: true,
      appBar: AppBar(
        title: AppText('Apply for Membership', style: AppTextStyles.heading1, copyWith: (c) => c.copyWith(color: Colors.white)),
      ),
      child: Container(
        decoration: BoxDecoration(
          color: AppColorPallete.whiteColor,
          borderRadius: BorderRadius.only(topLeft: AppTheme.AppCardRadius, topRight: AppTheme.AppCardRadius),
        ),
        padding: const EdgeInsets.all(15),
        child: asyncState.when(
          data: (state) => Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              FormStepper(activeStep: state.currentStep, stepNames: stepperList),
              const SizedBox(height: 20),
              Expanded(
                child: SingleChildScrollView(
                  child: Column(
                    children: [
                      switch (state.currentStep) {
                        0 => MembershipDetailsForm(state, controller, formKey: membershipFormKey),
                        1 => CertificateDetailsForm(state, controller, formKey: membershipFormKey),
                        2 => AttachmentsForm(state, controller, formKey: membershipFormKey),
                        _ => const SizedBox.shrink(),
                      },
                      SaveAsDraftButton(
                        onPressed: () {
                          controller.submitForm(context, serviceId: widget.serviceId, isDraft: true);
                        },
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
          loading: () => const Loader(),
          error: (e, st) => AppStateError(error: e),
        ),
      ),
      bottomNavigationBar: asyncState.whenOrNull(
        skipError: true,
        data: (state) => FormBottomNavigationBar(
          isFinalStep: state.currentStep == (stepperList.length - 1),
          onPreviousTap: state.currentStep >= 1
              ? () {
                  final isValid = membershipFormKey.currentState?.validate() ?? false;
                  if (isValid) {
                    membershipFormKey.currentState?.save();
                  }
                  controller.previousStep();
                }
              : null,
          onCancelTap: state.currentStep >= 1 ? null : () { controller.resetForm(); },
          onNextTap: () {
            final isValid = membershipFormKey.currentState?.validate() ?? false;
            if (isValid) {
              membershipFormKey.currentState?.save();
              controller.nextStep();
            }
          },
          onSubmit: () {
            final isValid = membershipFormKey.currentState?.validate() ?? false;
            if (isValid) {
              membershipFormKey.currentState?.save();
              controller.submitForm(context, serviceId: widget.serviceId);
            }
          },
        ),
      ),
    );
  }
}

MembershipDetailsForm 核心代码

class MembershipDetailsForm extends ConsumerStatefulWidget {
  final GlobalKey<FormState> formKey;
  final ApplyForMembershipState state;
  final ApplyForMembershipController controller;

  const MembershipDetailsForm(this.state, this.controller, {super.key, required this.formKey});

  @override
  ConsumerState<MembershipDetailsForm> createState() => _MembershipDetailsState();
}

class _MembershipDetailsState extends ConsumerState<MembershipDetailsForm> {
  bool _hasListenerAttached = false;
  bool _controllersInitialized = false;
  TextEditingController _identityTextCtrl = TextEditingController();
  TextEditingController _membershipTextCtrl = TextEditingController();

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final asyncState = ref.read(applyForMembershipControllerProvider());
    asyncState.whenData((state) {
      if (!_controllersInitialized) {
        _identityTextCtrl.text = state.form.identityNumber ?? '';
        _membershipTextCtrl.text = state.form.membershipNumber ?? '';
        _controllersInitialized = true;
        // Set default if null
        if (state.form.anyPreviousMembership == null) {
          ref.read(applyForMembershipControllerProvider().notifier).updateForm((form) => form.copyWith(anyPreviousMembership: false));
        }
      }
    });
  }

  @override
  void dispose() {
    _identityTextCtrl.dispose();
    _membershipTextCtrl.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final formKey = widget.formKey;

    // 临时测试用的监听,后续想删掉但暂时没找到更好的办法
    if (!_hasListenerAttached) {
      ref.listen<AsyncValue<ApplyForMembershipState>>(applyForMembershipControllerProvider(), (previous, next) {
        next.whenData((data) {
          debugPrint('邏MembershipDetailsForm邏 Updated From Lisner');
          _identityTextCtrl.text = data.form.identityNumber ?? '';
          _membershipTextCtrl.text = data.form.membershipNumber ?? '';
        });
      });
      _hasListenerAttached = true;
    }

    return Form(
      key: formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _identityTextCtrl,
            decoration: const InputDecoration(labelText: 'Identity Number'),
            validator: (value) => value?.isEmpty ?? true ? 'Please enter identity number' : null,
            onSaved: (value) => widget.controller.updateForm((form) => form.copyWith(identityNumber: value)),
          ),
          TextFormField(
            controller: _membershipTextCtrl,
            decoration: const InputDecoration(labelText: 'Membership Number'),
            onSaved: (value) => widget.controller.updateForm((form) => form.copyWith(membershipNumber: value)),
          ),
          // 其他表单字段...
        ],
      ),
    );
  }
}

Provider 大致结构(@riverpod生成)

@riverpod
class ApplyForMembershipController extends _$ApplyForMembershipController {
  @override
  FutureOr<ApplyForMembershipState> build({String? applicationId}) async {
    // 这里的API调用会执行两次
    final initialData = await ref.read(apiServiceProvider).getMembershipApplication(applicationId);
    return ApplyForMembershipState(
      currentStep: 0,
      form: initialData.form,
    );
  }

  void updateForm(ApplyForMembershipForm Function(ApplyForMembershipForm) update) {
    state = AsyncData(state.value!.copyWith(form: update(state.value!.form)));
  }

  // nextStep、previousStep、submitForm等其他方法...
}

我的疑问

  1. 为什么Provider的build方法会触发两次?有没有办法彻底避免重复的API请求?
  2. 表单输入框和Riverpod状态之间的同步,有没有更优雅的实现方式?不想用这么多TextEditingController和监听来兜底...

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 11:24:34