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

在表单状态管理中避免BlocBuilder不必要的重建

表单状态更新导致所有BlocBuilder不必要重建的问题

我在Flutter项目中使用Bloc管理表单状态,表单包含TextFormField、下拉选择器等多种输入组件,每个组件都配置了onChange事件并绑定到Bloc事件,所有组件都用BlocBuilder包裹。状态类FormStat通过formData这个Map存储用户的所有输入内容。

目前的问题是:每当formData更新并触发状态发射时,所有订阅该状态的BlocBuilder都会触发重建。我尝试使用buildWhen来过滤不必要的重建,但没有达到预期效果。相关代码如下:

class BuildFormFields {
  static Widget buildFormField(FormItem field, BuildContext context) {
    var temp = '''{
      "data_1": "option 1",
      "data_2": "option 2",
      "data_3": "option 3"
}''';

    Map<String, dynamic> payload = jsonDecode(temp);
    Map<String, Future<Map<String, dynamic>>> dropdownOptions = {};
    dropdownOptions['dropdownOptions'] = Future.value(payload);

    final formBloc = context.read<FormBloc>();
    switch (field.type) {
      case "textfield":
        formBloc
            .add(FormEventSetTextFormController(controller: field.variable!));
        return BlocBuilder<FormBloc, FormStat>(buildWhen: (previous, current) {
          log("pre ${previous.formData[field.variable]} ,${field.variable}");
          log("curr ${current.formData[field.variable]},${field.variable}");
          return previous.controllers[field.variable]?.text ==
              current.controllers[field.variable]?.text;
        }, builder: (context, state) {
          log("REBUILD Happened textfield ${field.variable}, ${state.formData}");
          return CustomTextFormField(
            initialValue: state.controllers[field.variable]?.text,
            isRequired: field.isRequired ?? true,
            fieldName: field.enLabel!,
            controller: state.controllers[field.variable],
            onSaved: (value) {
              formBloc.add(FormEventOnSavedClickOrOnChanged(
                  variable: field.variable!.trim(), value: value));
            },
            onChanged: (value) {
              formBloc.add(FormEventOnSavedClickOrOnChanged(
                  variable: field.variable!.trim(), value: value));
            },
            keyboardType: TextInputType.text,
          );
        });
      case "dropdown":
        log("dropdown");
        return BlocBuilder<FormBloc, FormStat>(builder: (context, state) {
          return CustomDropdown(
            formData: state.formData,
            field: field,
            parentNotifier: (variable) {
              // _parentNotifier(variable);
            },
            onChanged: (value) {
              log("dropdown_onchanged_value ${value}");
              formBloc.add(FormEventOnSavedClickOrOnChanged(
                  variable: field.variable!.trim(), value: value));
            },
            dropdownOptions: dropdownOptions,
          );
        });
      case "textview":
        formBloc
            .add(FormEventSetTextFormController(controller: field.variable!));
        formBloc.add(FormEventRegisterDependencies(
            dependencies: field.dependencies!, variableName: field.variable));
        return BlocBuilder<FormBloc, FormStat>(builder: (context, state) {
          bool dependenciesMet = true;
          if (dependenciesMet) {
            formBloc.add(FormEventApiCalls(endpoint: field.endpoint));
            return CustomTextFormField(
              isRequired: field.isRequired ?? true,
              fieldName: field.enLabel!,
              readOnly: true,
              controller: state.controllers[field.variable],
              keyboardType: TextInputType.text,
            );
          }
          return Container();
        });
      case "single_checkbox_bool":
        return BlocBuilder<FormBloc, FormStat>(builder: (context, state) {
          return Text("single_checkbox_bool");
        });
      default:
        return Text("Unknown widget Type");
    }
  }
}

@freezed
class FormStat with _$FormStat {
  const factory FormStat(
      {AppForm? baseFormUiModel,
      dynamic stepFormUiModel,
      @Default({}) Map<String, dynamic> formData,
      @Default({}) Map<String, TextEditingController> controllers,
      @Default({}) Map<String, Set<String>> registrar}) = _FormStat;
}

FutureOr<void> onFormEventOnSavedClickOrOnChanged(
    FormEventOnSavedClickOrOnChanged event,
    Emitter<FormStat> emit,
  ) async {
    final updatedFormData = Map<String, dynamic>.from(state.formData);
    final updatedContoller =
        Map<String, TextEditingController>.from(state.controllers);

    updatedFormData[event.variable] = event.value;
    updatedContoller[event.variable]?.text = event.value!;

    emit(state.copyWith(
        controllers: updatedContoller, formData: updatedFormData));

    log("Updated Form Data: ${state.formData}");
  }

问题修复方案

1. 修正textfield的buildWhen逻辑

你当前的buildWhen条件写反了,它表示“前后状态的控制器文本相同时才重建”,但我们需要的是当前字段内容变化时才重建,所以修改为:

return BlocBuilder<FormBloc, FormStat>(
  buildWhen: (previous, current) {
    return previous.controllers[field.variable]?.text != current.controllers[field.variable]?.text;
  },
  builder: (context, state) {
    // 原有代码
  },
);

2. 给其他组件添加buildWhen过滤

除了textfield,其他组件的BlocBuilder都没有过滤条件,导致任何状态更新都会触发重建:

  • Dropdown组件:只在当前字段的formData值变化时重建
return BlocBuilder<FormBloc, FormStat>(
  buildWhen: (previous, current) {
    return previous.formData[field.variable] != current.formData[field.variable];
  },
  builder: (context, state) {
    // 原有Dropdown代码
  },
);
  • TextView组件:只在当前字段的控制器文本变化时重建
return BlocBuilder<FormBloc, FormStat>(
  buildWhen: (previous, current) {
    return previous.controllers[field.variable]?.text != current.controllers[field.variable]?.text;
  },
  builder: (context, state) {
    // 原有TextView代码
  },
);

3. 移除builder中的重复事件触发

在textview的BlocBuilder中,每次重建都会调用formBloc.add(FormEventApiCalls(...)),这会引发不必要的事件循环和重建。应该将事件移到组件初始化阶段:

将textview组件封装为StatefulWidget,在initState中执行一次性操作:

class TextViewFormField extends StatefulWidget {
  final FormItem field;
  const TextViewFormField({super.key, required this.field});

  @override
  State<TextViewFormField> createState() => _TextViewFormFieldState();
}

class _TextViewFormFieldState extends State<TextViewFormField> {
  @override
  void initState() {
    super.initState();
    final formBloc = context.read<FormBloc>();
    formBloc.add(FormEventSetTextFormController(controller: widget.field.variable!));
    formBloc.add(FormEventRegisterDependencies(
      dependencies: widget.field.dependencies!, 
      variableName: widget.field.variable
    ));
    // 仅初始化时触发一次API请求
    formBloc.add(FormEventApiCalls(endpoint: widget.field.endpoint));
  }

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<FormBloc, FormStat>(
      buildWhen: (previous, current) {
        return previous.controllers[widget.field.variable]?.text != current.controllers[widget.field.variable]?.text;
      },
      builder: (context, state) {
        bool dependenciesMet = true;
        if (dependenciesMet) {
          return CustomTextFormField(
            isRequired: widget.field.isRequired ?? true,
            fieldName: widget.field.enLabel!,
            readOnly: true,
            controller: state.controllers[widget.field.variable],
            keyboardType: TextInputType.text,
          );
        }
        return Container();
      },
    );
  }
}

4. 优化状态更新逻辑

在onFormEventOnSavedClickOrOnChanged中,无需复制整个controllersMap,因为TextEditingController是可变对象,直接修改其文本即可,仅更新formData并发射状态:

FutureOr<void> onFormEventOnSavedClickOrOnChanged(
    FormEventOnSavedClickOrOnChanged event,
    Emitter<FormStat> emit,
  ) async {
    final updatedFormData = Map<String, dynamic>.from(state.formData);
    updatedFormData[event.variable] = event.value;

    // 直接修改目标控制器文本,无需复制整个Map
    state.controllers[event.variable]?.text = event.value!;

    emit(state.copyWith(formData: updatedFormData));
    log("Updated Form Data: ${updatedFormData}");
  }

内容的提问来源于stack exchange,提问作者Pe be

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 15:12:02