Search code examples
flutterdropdownbutton

Why the DropDownButtonFormField does not need a setState to to rebuild, while the DropDownButton needs it?


I tried to figure it out, and read the documentation for both but I didn't find an answer, here is an example of what I mean:

List<String> items = ["item1", "item2", "item3", "item4"];
class HomeScreen extends StatelessWidget {
  HomeScreen({super.key});
  String selectedItem = items[0];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Padding(
            padding: const EdgeInsets.all(20.0),
            child: DropdownButton(
              value: selectedItem,
              onChanged: (value) => selectedItem = value!,
              items: items
                  .map(
                    (e) => DropdownMenuItem<String>(
                      value: e,
                      child: Text(e),
                    ),
                  )
                  .toList(),
            ),
...

that's just a simple stateless widget with a DropdownButton at the center: output of the code above

if we just change the widget to a DropdownButtonFormField with all else remain the same, changes to the selected item reflect in the UI: output of the same code after changing the widget to a DropdownButtonFormField


Solution

  • If you dig inside DropdownButtonFormField you will see it keeps a separate value for the menu inside its state. If you explore the code it says

    onChanged: onChanged == null ? null : state.didChange,
    

    state.didChange looks like:

      @override
      void didChange(T? value) {
        super.didChange(value);
        final DropdownButtonFormField<T> dropdownButtonFormField = widget as DropdownButtonFormField<T>;
        assert(dropdownButtonFormField.onChanged != null);
        dropdownButtonFormField.onChanged!(value);
      }
    

    and super.didChange looks like

      void didChange(T? value) {
        setState(() {
          _value = value;
          _hasInteractedByUser.value = true;
        });
        Form.of(context)?._fieldDidChange();
      }
    

    This changes the iternal value of the state and calls setState so that it refreshes the UI for it.

    As a result, even if you change this line of your code:

    onChanged: (value) => selectedItem = value!,
    

    to

    onChanged: (value){},
    

    It still works visually, because it doesn't actually use selectedItem but the internal value.