Repository: flutter-form-builder-ecosystem/flutter_form_builder

Issue:
FormBuilderDropdown onChanged fires twice during form Reset
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Package/Plugin version

9.2.1

### Platforms

- [X] Android
- [X] iOS
- [ ] Linux
- [X] MacOS
- [ ] Web
- [X] Windows

### Flutter doctor

<details>
  <summary>Flutter doctor</summary>

<!-- Paste the output below inside ``` and ``` -->
```bash

```

</details>


### Minimal code example

<details>
  <summary>Code sample</summary>

<!-- Paste the output below inside ``` and ``` -->

```dart
FormBuilder(
    child: Column(
      children: [
        Builder(
          builder: (context) {
            return ElevatedButton(onPressed: (){
              FormBuilder.of(context)!.reset();
            }, child: Text('reset'));
          }
        ),
        FormBuilderDropdown(
          name: 'test', 
          initialValue: 'A', 
          items: [
            DropdownMenuItem(child: Text('A'), value: 'A'),
            DropdownMenuItem(child: Text('B'), value: 'B')
          ],
          onChanged: (value) {
            print('Changed value to $value');
          },
        ),
      ],
    ),
  ),
```

</details>


### Current Behavior

When resetting the form, the dropdowns "onChange" handler will fire twice in quick succession - one setting it to the "initialValue", then a second setting it back to the value it was before the reset button was clicked (essentially undoing the reset).

### Expected Behavior

When resetting the form, the "onChange" should fire only once, and the passed value should be the "initialValue".

### Steps To Reproduce

Using the above code snippet.

1. Run the code
2. Click the dropdown and change its value to "B"
3. Press the "reset" button

Note that it will not reset, but stay as "B", and the debug console will read

```
flutter: Changed value to A
flutter: Changed value to B
```

### Aditional information

_No response_
[FormBuilderDropdown]: Not reverting to initial value upon form reset
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Package/Plugin version

9.3.0

### Platforms

- [ ] Android
- [X] iOS
- [ ] Linux
- [ ] MacOS
- [ ] Web
- [ ] Windows

### Flutter doctor

<details>
  <summary>Flutter doctor</summary>

<!-- Paste the output below inside ``` and ``` -->
```bash
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.22.1, on macOS 14.5 23F79 darwin-arm64, locale en-AU)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 15.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.1)
[✓] IntelliJ IDEA Community Edition (version 2023.2.2)
[✓] VS Code (version 1.89.1)
[✓] Connected device (5 available)
[✓] Network resources

• No issues found!
```

</details>


### Minimal code example

<details>
  <summary>Code sample</summary>

<!-- Paste the output below inside ``` and ``` -->

```dart
class MyForm extends StatefulWidget {
  const MyForm({super.key});

  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormBuilderState>();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: FormBuilder(
        key: _formKey,
        child: Column(
          children: <Widget>[
            FormBuilderDropdown(
              name: 'dropdown',
              decoration: const InputDecoration(
                labelText: 'Select Option',
              ),
              // hint: Text('Select Option'), // Optional
              items: const [
                DropdownMenuItem(
                  value: 'Option 1',
                  child: Text('Option 1'),
                ),
                DropdownMenuItem(
                  value: 'Option 2',
                  child: Text('Option 2'),
                ),
                DropdownMenuItem(
                  value: 'Option 3',
                  child: Text('Option 3'),
                ),
              ],
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                print("BEFORE RESET");
                print(_formKey.currentState?.value);
                _formKey.currentState?.reset();
                print("AFTER RESET");
                print(_formKey.currentState?.value);
              },
              child: const Text('Reset'),
            ),
          ],
        ),
      ),
    );
  }
}
```

</details>


### Current Behavior

The dropdown field is not reverting to the initial value upon form reset. It instead retains the value that has been selected prior to reset. 

### Expected Behavior

Dropdown field  should be reset to initial value when the form is reset

### Steps To Reproduce

1. Run app
2. Dropdown field has initial value as null. Select an option
3. Press reset button

### Aditional information

```bash
flutter: BEFORE RESET
flutter: {dropdown: Option 2}
flutter: AFTER RESET
flutter: {dropdown: Option 2}
```


Discussion: Additional information:

In my actual app (I'm assuming this is what's going on inside the demo code I provided too) - I have 4 FormBuilder fields.
2 * FormBuilderTextField
2 * FormBuilderDropdown

When I breakpoint the onChange, I can see that the form actually contains *6* field state objects - 2 for the text fields, and 4 for the dropdowns... it looks like for each dropdown there is a "FormBuilderDropdownState" and a "DropdownButtonFormFieldState".

When 'reset' is called on the FormBuilderDropdownState, it correctly fires onChanged and passes the initialValue.
However, when 'reset' is then called on the DropdownButtonFormFieldState this is when it resets it back to its current value.

I suspect this second state class isn't supposed to be in the fields collection - as it's just a double-up, one that breaks the functionality at that.

<img width="740" alt="image" src="https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/assets/16373742/579c4265-47c4-449a-be1c-b18a5363db5a">

> Additional information:
> 
> In my actual app (I'm assuming this is what's going on inside the demo code I provided too) - I have 4 FormBuilder fields. 2 * FormBuilderTextField 2 * FormBuilderDropdown
> 
> When I breakpoint the onChange, I can see that the form actually contains _6_ field state objects - 2 for the text fields, and 4 for the dropdowns... it looks like for each dropdown there is a "FormBuilderDropdownState" and a "DropdownButtonFormFieldState".
> 
> When 'reset' is called on the FormBuilderDropdownState, it correctly fires onChanged and passes the initialValue. However, when 'reset' is then called on the DropdownButtonFormFieldState this is when it resets it back to its current value.
> 
> I suspect this second state class isn't supposed to be in the fields collection - as it's just a double-up, one that breaks the functionality at that.
> 
> <img alt="image" width="740" src="https://private-user-images.githubusercontent.com/16373742/306903650-579c4265-47c4-449a-be1c-b18a5363db5a.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjYzMjI1MjAsIm5iZiI6MTcyNjMyMjIyMCwicGF0aCI6Ii8xNjM3Mzc0Mi8zMDY5MDM2NTAtNTc5YzQyNjUtNDdjNC00NDlhLWJlMWMtYjE4YTUzNjNkYjVhLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MTQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTE0VDEzNTcwMFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWEwOTM5MDZmYzlmMjVmYTkyYTFjY2ViNzJmZDMxOWNiMDg0YTgyMzEzODAyNWU0OTg2NDM0ZTQxMjU5Y2FiZmUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.21PylZnzODQllwzvTWr9ErI8s8eyQv7MWOvK721EykA">

Yeah but DropdownButtonFormField also extends FormField, In FormField's build function we are registering this field. So even this gets registered

If we comment out the reset function for DropdownButtonFormFieldState it works.

Also if we replace DropdownButtonFormField with DropdownButton it works fine, since DropdownButton is not a form field, it will not register it's field. But DropdownButton doesn't have a decoration parameter. I think we should go with this as we're unnecessarily creating an extra field if we use DropdownButtonFormField.
Could be related to this issue - https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/pull/1389
I think this is just a bug. When reset() is called, onChange gets called twice for FormBuilderDropdown widgets: first with the correct (original) value, then with the incorrect (current value).  Net result: the value doesn't get reset to the original value. (flutter_form_builder: ^9.4.1)
... which is the same issue mentioned here: https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/issues/1371
I test this and using reset by field, `_formKey.currentState?.fields['dropdown']?.reset()`, works fine.
I debugged why Dropdown don't reset but I don't have a conclusion. The only thing that I found is that on form.dart (from Flutter core), had two Dropdown states. One on FormBuilderDropdown and other for DropdownButtonFormFieldState. Maybe the bug is related with that

![image](https://github.com/user-attachments/assets/be56aa68-6e63-4e0d-9788-8e77ed9717ef)

While I writing the previous comment, I notice that I used DropdownButtonFormField instead DropdownButton.
This created a conflict between the FormBuilder state (package) and the Form Flutter state (core).

Fix patch:
diff --git a/lib/src/fields/form_builder_dropdown.dart b/lib/src/fields/form_builder_dropdown.dart
index 6a3250380..a70151dac 100644
--- a/lib/src/fields/form_builder_dropdown.dart
+++ b/lib/src/fields/form_builder_dropdown.dart
@@ -163,6 +163,12 @@ class FormBuilderDropdown<T> extends FormBuilderFieldDecoration<T> {
   /// [kMinInteractiveDimension].
   final double? itemHeight;
 
+  /// The width of the menu.
+  ///
+  /// If it is not provided, the width of the menu is the width of the
+  /// dropdown button.
+  final double? menuWidth;
+
   /// The color for the button's [Material] when it has the input focus.
   final Color? focusColor;
 
@@ -175,6 +181,17 @@ class FormBuilderDropdown<T> extends FormBuilderFieldDecoration<T> {
   /// instead.
   final Color? dropdownColor;
 
+  /// Padding around the visible portion of the dropdown widget.
+  ///
+  /// As the padding increases, the size of the [DropdownButton] will also
+  /// increase. The padding is included in the clickable area of the dropdown
+  /// widget, so this can make the widget easier to click.
+  ///
+  /// Padding can be useful when used with a custom border. The clickable
+  /// area will stay flush with the border, as opposed to an external [Padding]
+  /// widget which will leave a non-clickable gap.
+  final EdgeInsetsGeometry? padding;
+
   /// The maximum height of the menu.
   ///
   /// The maximum height of the menu must be at least one row shorter than
@@ -227,6 +244,11 @@ class FormBuilderDropdown<T> extends FormBuilderFieldDecoration<T> {
   /// this widget is used as the placeholder.
   final Widget? hint;
 
+  /// The widget to use for drawing the drop-down button's underline.
+  ///
+  /// Defaults to SizedBox.shrink().
+  final Widget? underline;
+
   /// Creates field for Dropdown button
   FormBuilderDropdown({
     super.key,
@@ -263,43 +285,54 @@ class FormBuilderDropdown<T> extends FormBuilderFieldDecoration<T> {
     this.borderRadius,
     this.alignment = AlignmentDirectional.centerStart,
     this.hint,
+    this.underline = const SizedBox.shrink(),
+    this.padding,
+    this.menuWidth,
   }) : super(
           builder: (FormFieldState<T?> field) {
             final state = field as _FormBuilderDropdownState<T>;
 
             final hasValue = items.map((e) => e.value).contains(field.value);
-            return DropdownButtonFormField<T>(
-              isExpanded: isExpanded,
+            return InputDecorator(
               decoration: state.decoration,
-              items: items,
-              value: hasValue ? field.value : null,
-              style: style,
-              isDense: isDense,
-              disabledHint: hasValue
-                  ? items
-                      .firstWhere(
-                          (dropDownItem) => dropDownItem.value == field.value)
-                      .child
-                  : disabledHint,
-              elevation: elevation,
-              iconSize: iconSize,
-              icon: icon,
-              iconDisabledColor: iconDisabledColor,
-              iconEnabledColor: iconEnabledColor,
-              onChanged:
-                  state.enabled ? (T? value) => state.didChange(value) : null,
-              onTap: onTap,
-              focusNode: state.effectiveFocusNode,
-              autofocus: autofocus,
-              dropdownColor: dropdownColor,
-              focusColor: focusColor,
-              itemHeight: itemHeight,
-              selectedItemBuilder: selectedItemBuilder,
-              menuMaxHeight: menuMaxHeight,
-              borderRadius: borderRadius,
-              enableFeedback: enableFeedback,
-              alignment: alignment,
-              hint: hint,
+              child: DropdownButton<T>(
+                menuWidth: menuWidth,
+                padding: padding,
+                underline: underline,
+                isExpanded: isExpanded,
+                items: items,
+                value: hasValue ? field.value : null,
+                style: style,
+                isDense: isDense,
+                disabledHint: hasValue
+                    ? items
+                        .firstWhere(
+                            (dropDownItem) => dropDownItem.value == field.value)
+                        .child
+                    : disabledHint,
+                elevation: elevation,
+                iconSize: iconSize,
+                icon: icon,
+                iconDisabledColor: iconDisabledColor,
+                iconEnabledColor: iconEnabledColor,
+                onChanged: state.enabled
+                    ? (T? value) {
+                        field.didChange(value);
+                      }
+                    : null,
+                onTap: onTap,
+                focusNode: state.effectiveFocusNode,
+                autofocus: autofocus,
+                dropdownColor: dropdownColor,
+                focusColor: focusColor,
+                itemHeight: itemHeight,
+                selectedItemBuilder: selectedItemBuilder,
+                menuMaxHeight: menuMaxHeight,
+                borderRadius: borderRadius,
+                enableFeedback: enableFeedback,
+                alignment: alignment,
+                hint: hint,
+              ),
             );
           },
         );


Test patch:
diff --git a/test/src/fields/form_builder_dropdown_test.dart b/test/src/fields/form_builder_dropdown_test.dart
index 0dbdc0230..a582bb01e 100644
--- a/test/src/fields/form_builder_dropdown_test.dart
+++ b/test/src/fields/form_builder_dropdown_test.dart
@@ -225,6 +225,47 @@ void main() {
       expect(formValue(widgetName), equals(2));
     });
   });
+  testWidgets('Should reset to null when call reset', (tester) async {
+    const widgetName = 'dropdown_field';
+
+    // Define the initial and updated items for the dropdown
+    const List<DropdownMenuItem<int>> initialItems = [
+      DropdownMenuItem(value: 1, child: Text('Option 1')),
+      DropdownMenuItem(value: 2, child: Text('Option 2')),
+    ];
+
+    final testWidget =
+        FormBuilderDropdown(name: widgetName, items: initialItems);
+    await tester.pumpWidget(buildTestableFieldWidget(testWidget));
+
+    formKey.currentState?.patchValue({widgetName: 1});
+    await tester.pumpAndSettle();
+    formKey.currentState?.reset();
+
+    expect(formKey.currentState?.instantValue, {widgetName: null});
+  });
+  testWidgets('Should reset to initial when call reset', (tester) async {
+    const widgetName = 'dropdown_field';
+    const initialValue = {widgetName: 1};
+
+    // Define the initial and updated items for the dropdown
+    const List<DropdownMenuItem<int>> initialItems = [
+      DropdownMenuItem(value: 1, child: Text('Option 1')),
+      DropdownMenuItem(value: 2, child: Text('Option 2')),
+    ];
+    final testWidget =
+        FormBuilderDropdown(name: widgetName, items: initialItems);
+    await tester.pumpWidget(buildTestableFieldWidget(
+      testWidget,
+      initialValue: initialValue,
+    ));
+
+    formKey.currentState?.patchValue({widgetName: 2});
+    await tester.pumpAndSettle();
+    formKey.currentState?.reset();
+
+    expect(formKey.currentState?.instantValue, equals(initialValue));
+  });
 }
 
 class MyTestWidget<T> extends StatefulWidget {