Search code examples
flutterformsblocflutter-blocflutter-reactive-forms

Why is Bloc state not updating when using reactive_forms in flutter?


I am using flutter with reactive_forms package and BLoC. I tried to outsource the FormGroup to BLoC state class and providing it to the ReactiveFormBuilder using BlocBuilder widget.

Flutter Bloc - Profile State:

BLOC STATE

class ProfileState extends Equatable {
  final initAdditionalDocForm = FormGroup({
    "doc_images": FormControl(),
    "doc_number": FormControl(),
    "doc_type_id": FormControl()
  });
  final FormGroup initPersonalForm = FormGroup({
    'op_first_name': FormControl<String>(
        validators: [Validators.minLength(4), Validators.required]),
    'op_last_name': FormControl<String>(
        validators: [Validators.minLength(4), Validators.required]),
    'op_dob': FormControl<DateTime>(validators: [Validators.required]),
    'op_gender': FormControl<String>(
        validators: [Validators.required], value: "Not Specified"),
    'op_email': FormControl<String>(
        validators: [Validators.required, Validators.email]),
    'op_pet_name': FormControl<String>(),
    'op_mobile_no': FormControl<int>(validators: [
      Validators.number,
    ]),
    'op_alternate_mobile_no': FormControl<int>(validators: [
      Validators.number,
    ]),
    'veh_driving_license_no':
        FormControl<String>(validators: [Validators.required]),
    'veh_license_validity':
        FormControl<DateTime>(validators: [Validators.required]),
    'op_pan_no': FormControl<String>(),
    'AdditionalDoc': FormArray([]),
    'op_address_pin_code': FormControl<String>(
        validators: [Validators.required, Validators.number]),
    'op_address_state': FormControl<String>(validators: [Validators.required]),
    'op_address_city': FormControl<String>(validators: [Validators.required]),
    'op_address_line_1': FormControl<String>(validators: [Validators.required]),
    'op_address_line_2': FormControl<String>(),
    'op_landmark': FormControl<String>(),
    'op_profile': FormControl<String>() //Base 64 Profile Picture
  });

  static const _vehNoRegex =
      r"^[A-Z]{2}[ -][0-9]{1,2}(?: [A-Z])?(?: [A-Z]*)? [0-9]{4}$";

  final initVehicleInfoForm = FormGroup({
    //vehicle images maintained in the screen it self due to naming convention of API
    'veh_no_person': FormControl<int>(validators: [Validators.number]),
    'veh_charge_per_person':
        FormControl<int>(validators: [Validators.number, Validators.min(20)]),
    'veh_registration_no': FormControl<String>(
        validators: [Validators.pattern(_vehNoRegex), Validators.required]),
    'veh_city': FormControl<String>(validators: [Validators.required]),
    'veh_wheel_type': FormControl<int>(value: 4),
    'veh_capacity': FormControl<int>(),
    'veh_dimension': FormControl<String>(),
    'veh_color': FormControl<String>(validators: [Validators.required]),
    'veh_type':
        FormControl<int>(validators: [Validators.required], value: 1), //1 or 2
    'veh_fuel_type': FormControl<String>(validators: [Validators.required]),
    'veh_3km_15km': FormControl<int>(validators: [Validators.number]),
    'veh_above_15km': FormControl<int>(validators: [Validators.number]),
    'AdditionalDoc': FormArray([]),
  });

  final initBusinessProfileForm = FormGroup({
    'doc_pan':
        FormControl<String>(validators: [Validators.required]), //Base64 Image
    'op_bu_address_city':
        FormControl<String>(validators: [Validators.required]),
    'op_bu_address_line_1':
        FormControl<String>(validators: [Validators.required]),
    'op_bu_address_line_2':
        FormControl<String>(validators: [Validators.required]),
    'op_bu_address_pin_code':
        FormControl<int>(validators: [Validators.required]),
    'op_bu_address_state':
        FormControl<String>(validators: [Validators.required]),
    'op_payment_mode': FormControl<String>(validators: [Validators.required]),
    'op_bu_email': FormControl<String>(
        validators: [Validators.required, Validators.email]),
    'op_bu_gstn_available':
        FormControl<bool>(validators: [Validators.required]),
    'op_bu_gstn_no': FormControl<String>(validators: [Validators.required]),
    'op_bu_landmark': FormControl<String>(validators: [Validators.required]),
    'op_bu_name': FormControl<String>(validators: [Validators.required]),
    'op_bu_pan_no': FormControl<String>(validators: [Validators.required]),
    'AdditionalDoc': FormArray([])
  });

  ///Bank Details
  final initPaymentInfoForm = FormGroup({
    'op_bank_name': FormControl<String>(validators: [Validators.required]),
    'op_bank_ifsc': FormControl<String>(validators: [Validators.required]),
    'op_bank_account_number':
        FormControl<int>(validators: [Validators.required]),
  });

  late final FormGroup personalProfileForm;
  late final FormGroup personalProfileAdditionalDocGroup;
  late final FormGroup vehicleInfoForm;
  late final FormGroup businessProfileForm;
  late final FormGroup paymentInfoForm;

  ProfileState({
    FormGroup? personalForm,
    FormGroup? additionalDoc,
    FormGroup? vehicleForm,
    FormGroup? businessForm,
    FormGroup? paymentForm,
  }) : super() {
    personalProfileAdditionalDocGroup = additionalDoc ?? initAdditionalDocForm;
    personalProfileForm = personalForm ?? initPersonalForm;
    vehicleInfoForm = vehicleForm ?? initVehicleInfoForm;
    businessProfileForm = businessForm ?? initBusinessProfileForm;
    paymentInfoForm = paymentForm ?? initPaymentInfoForm;
  }

  @override
  List<Object> get props => [
        personalProfileForm,
        personalProfileAdditionalDocGroup,
        businessProfileForm,
        vehicleInfoForm,
        paymentInfoForm,
      ];

  ProfileState copyWith({
    FormGroup? personalForm,
    FormGroup? additionalDoc,
    FormGroup? vehicleForm,
    FormGroup? businessForm,
    FormGroup? paymentForm,
  }) =>
      ProfileState(
        personalForm: personalForm ?? personalProfileForm,
        additionalDoc: additionalDoc ?? personalProfileAdditionalDocGroup,
        vehicleForm: vehicleInfoForm,
        businessForm: businessProfileForm,
        paymentForm: paymentInfoForm,
      );
}

BLOC Method

Inside the main Bloc Class there is a event method which is fired when the form is filled.

 Future<void> _onSaveProfile(ProfileEvent event, Emitter emit) async {
    try {
      emit(ProfileSaving());
      if (event is SaveProfileData) {
        state.copyWith(personalForm: event.personalProfile);
      }

      emit(ProfileSaved());
    } catch (e) {
      print(e);
    }
  }

UI Widget

The reactive form builder is:

BlocBuilder<ProfileBloc, ProfileState>(
      builder: (context, state) {
        return ReactiveFormBuilder(
          form: () => state.personalProfileForm,
          builder: (context, form, _) => ListView(
            children: [
              /* First & Last Name */
              profileRowBuilder(
                context,
                const ReactiveTextFieldCustomV1(
                  formControlName: "op_first_name",
                  hintText: "",
                  showLabel: true,
                  labelText: "First Name",
                  shortHeight: true,
                ),
   ........
ElevatedButton(
                onPressed: () {
                  
                  context
                      .read<ProfileBloc>()
                      .add(SaveProfileData(personalProfile: form));
                  // print(
                  //     state.personalProfileForm.control('op_first_name').value);
                  // print(_drivingLicenseNo);
                },
                child: Text("Press"),
              ),
              ElevatedButton(
                onPressed: () {
                  print(
                      state.personalProfileForm.control('op_first_name').value);
                  // state.personalProfileForm.controls.forEach((key, value) {
                  //   print("$key: ${value.value}");
                  // });
                },
                child: Text("State"),
              ),
            ],
          ),
        );
      },
    );
  }

The first button is firing the event and I am passing the entire form to the event and as shown in the _onProfileSave() method, it is copied to state. But the form values which are typed in the textfield are null when I click the 2nd button to view the state.

Would be great to receive a solution. Also would be great to receive any suggestions on how to use reactive_forms with BLoC.

I tried to debug it and check via VS Code debugger, but the form inside of ReactiveBuilder there are values but when the event is fired, in the state the form values are still null. They are not copied.


Solution

  • As bloc requires an event to be fired when updating the state, it had to fire an event when then form changes and use .copyWith (if you have it setup). As this overhead is not necessary in Cubits, it works with cubits. So I generally follow Cubits for such use cases and Blocs for others like auth etc.