Skip to content

[material_ui] Add enabled parameter to DropdownButtonFormField - #12610

Merged
auto-submit[bot] merged 30 commits into
flutter:mainfrom
Mairramer:feature/remove-required-onchanged
Oct 13, 2026
Merged

auto-submit[bot] merged 30 commits into
flutter:mainfrom
Mairramer:feature/remove-required-onchanged

Conversation

@Mairramer

@Mairramer Mairramer commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Introduced the enabled parameter in DropdownButtonFormField, allowing proper disabled/read-only usage without needing to pass dummy callbacks or remove existing ones.

Previously, the only way to disable a DropdownButtonFormField was to set onChanged: null. With this change, developers can explicitly control the interactive state via the enabled property. For example, you can set enabled: false while keeping onChanged defined, or set enabled: true to have an interactive field even if onChanged is omitted.

This change is fully backward compatible. If the enabled property is omitted (left as null), the field will fall back to the legacy behavior: it will be enabled if and only if onChanged is non-null.

Closes flutter/flutter#57953.

Port of flutter/flutter#182419.

Pre-Review Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2

@Mairramer Mairramer added the CICD Run CI/CD label Oct 1, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 1, 2026
@Mairramer Mairramer added the CICD Run CI/CD label Oct 1, 2026
@Mairramer
Mairramer marked this pull request as ready for review October 1, 2026 19:01
@Mairramer
Mairramer requested a review from dkwingsmt October 1, 2026 19:02

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request makes the onChanged parameter optional and introduces an enabled property to both DropdownButton and DropdownButtonFormField to control their interactivity, along with corresponding updates to documentation, migration fixes, and tests. Feedback on the changes highlights a critical issue in DropdownButtonFormField where providing enabled: true with a null onChanged callback prevents the form field's value from updating when an item is selected. To resolve this, it is recommended to pass state.didChange to the underlying DropdownButton whenever the form field is enabled, and to add a test case verifying this behavior.

barrierDismissible: barrierDismissible,
mouseCursor: mouseCursor,
dropdownMenuItemMouseCursor: dropdownMenuItemMouseCursor,
enabled: isDropdownEnabled,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a critical bug here when DropdownButtonFormField is used with onChanged: null (or not provided) and enabled: true.\n\nIn the DropdownButtonFormField builder, the onChanged argument passed to DropdownButton is likely still defined as:\ndart\nonChanged: onChanged == null ? null : state.didChange,\n\nIf onChanged is null, this evaluates to null, so DropdownButton receives onChanged: null. Even though enabled: isDropdownEnabled (which is true) is passed to DropdownButton, making it interactive, selecting an item will do nothing because its onChanged callback is null. As a result, state.didChange is never called, and the form field's value is never updated.\n\nTo fix this, the onChanged argument passed to DropdownButton inside the builder should be updated to:\ndart\nonChanged: field.widget.enabled ? state.didChange : null,\n\nThis ensures that state.didChange is always called when the dropdown is enabled, which in turn updates the form field's value and safely calls the optional widget.onChanged callback if it is provided.

);
}).toList(),
alignment: buttonAlignment,
enabled: enabled,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Please add a test case to verify that DropdownButtonFormField works correctly when onChanged is null but enabled is true.\n\nSpecifically, the test should assert that:\n1. The dropdown is enabled and interactive.\n2. Selecting an item successfully updates the form field's value (i.e., formFieldKey.currentState.value changes to the selected item).\n\nThis will help prevent regressions and ensure that the new feature (making onChanged optional for read-only/disabled form field usage) works as intended.

@QuncCccccc

Copy link
Copy Markdown
Contributor

[from triage] Could you help fix the conflict:)?

# Conflicts:
#	packages/material_ui/lib/src/dropdown.dart
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 7, 2026
@Mairramer Mairramer added the CICD Run CI/CD label Oct 7, 2026

@dkwingsmt dkwingsmt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, I suggest adding a constructor assertion assert(enabled != true || onChanged != null), because in such cases the DropdownButton can be opened but any selection is dropped (since the button doesn't save its own values).

this.isVerticallyExpanded = false,
required this._inputDecoration,
required this._isEmpty,
bool? enabled,

@dkwingsmt dkwingsmt Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this new parameter not used in the constructor's only invocation at L1966?

IMO DropdownButtonFormField should pass down enabled and set onChanged as isDropdownEnabled ? state.didChange : null.

DropdownButtonFormField(onChanged: (String? v) {});

// Changes made in https://github.com/flutter/flutter/pull/182419.
DropdownButton(onChanged: null, enabled: true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These look like strange cases here because enabled did not even exist before this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, at the current changes I suspect that this PR is no longer breaking and should not need a dartfix.

// When adding new arguments, consider adding similar arguments to
// DropdownButtonFormField.
}) : assert(
}) : enabled = enabled ?? onChanged != null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest keeping enabled as bool?. While technically Dart allows constructor's arguments to be independent of the getters, it has been a convention of Flutter that they're identical and making them different like this will be confusing. Considering that getters of widgets are rarely used I think it's ok to just keep it the simple way.

/// If this property is null, [WidgetStateMouseCursor.adaptiveClickable] will be used.
final MouseCursor? dropdownMenuItemMouseCursor;

/// Whether the [DropdownButton] is enabled.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc should also be updated to reflect the functionality of the bool? enabled parameter, not the calculated bool enabled effective value.

@dkwingsmt

Copy link
Copy Markdown
Contributor

Update: after thinking about this more, this comment replaces my earlier inline comments about DropdownButton.enabled (keeping it bool?, its docs, and the assert). I now think DropdownButton doesn't need a new parameter at all. My comment on L1086 still applies in spirit, but the fix is different from what it implied (see below).

I think most of the issues here come from mixing up two layers that handle "enabled" differently. It might help to look at each one separately.

Layer 1: DropdownButton

DropdownButton doesn't store a value. The parent passes value in, and onChanged is the only way a selection gets back out. So for this widget, onChanged is the output channel, not just a notification, and "enabled iff onChanged != null" is the right rule. Checkbox, Switch and Slider follow the same rule.

An enabled parameter doesn't fit well here:

  • enabled: true either repeats what onChanged != null already says, or creates a dropdown that opens but drops every selection.
  • Only enabled: false adds anything, and that is the same as writing onChanged: disabled ? null : callback.

The issue (flutter/flutter#57953) also only asks about the form field. So I'd suggest reverting the DropdownButton changes, including the new parameter on _formField. If there's demand for DropdownButton.enabled, it can be discussed separately.

Layer 2: DropdownButtonFormField

The form field does store the value, in FormFieldState. The user's onChanged is only an optional notification on top of that, like in TextFormField. This is the layer the issue is about: users want an enabled field without passing a dummy onChanged.

The form field can get there without any help from DropdownButton. Whenever the field is enabled, it gives the inner button its own output channel, state.didChange, and didChange already calls the user's onChanged if one was provided. Whether the user passed onChanged should only affect the fallback when enabled is null, not how the inner button is wired:

super(
  // FormField.enabled also controls validation. Only an explicit
  // `enabled: false` should turn validation off, so existing
  // `onChanged: null` fields keep validating like they do today.
  enabled: enabled ?? true,
  builder: (FormFieldState<T> field) {
    final bool isDropdownEnabled =
        (enabled ?? onChanged != null) && items != null && items.isNotEmpty;
    // ...
    DropdownButton<T>._formField(
      onChanged: isDropdownEnabled ? state.didChange : null,
      // ...
    );
  },
);

Compared to this, the current PR has three problems:

  • L1972 still passes onChanged == null ? null : state.didChange, so enabled: true without onChanged (the case the issue asks for) shows an enabled decoration but leaves the button disabled.
  • With enabled: false and an onChanged, the decoration and FormField are disabled, but the inner button is still interactive.
  • super(enabled: enabled ?? onChanged != null) turns off autovalidation for every existing onChanged: null field, which is a behavior change the PR doesn't mention.

About the constructor parameter and the getter: FormField already declares final bool enabled, and Dart doesn't allow a subclass to override it with bool?. The parameter can't be a non-nullable bool enabled = true either. It couldn't tell "not passed" apart from true, so it would either break existing onChanged: null fields or still require onChanged. So, like TextFormField, the constructor should take bool? enabled and pass the resolved value to super.enabled. The raw argument is still available in the builder closure, the same way onChanged and items are.

What this means for the rest of the PR

With enabled: null falling back to the old rule, this change is purely additive:

  • The "Breaking change" section in the description doesn't apply and can be removed.
  • The dart fix rules and test_fixes can be removed. The rule for onChanged: null, enabled: true would also have rewritten it to enabled: false.
  • The onChanged doc template can go back to its original wording. A doc comment on the new enabled should explain the fallback.
  • For tests:
    • Revert the changes to existing tests (including passing enabled to DropdownMenuItem in the buildDropdown helper). As they were, they cover the fallback path.
    • Add form-field tests for the new combinations: enabled: true without onChanged updates FormFieldState.value when an item is selected, and enabled: false with onChanged doesn't open the menu.
    • Add one more test that an onChanged: null field still autovalidates.

@dkwingsmt dkwingsmt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Forgot to post my comments regarding the changelog)

@dkwingsmt

Copy link
Copy Markdown
Contributor

I'd also suggest adding the following sentence to the end of onChanged's doc:

  /// If the constructor's `enabled` argument is true, the field is interactive even
  /// when [onChanged] is null.

Just a reminder that this hasn't been addressed. Otherwise the code LGTM.

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 12, 2026
@Mairramer Mairramer added the CICD Run CI/CD label Oct 12, 2026

@dkwingsmt dkwingsmt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@QuncCccccc QuncCccccc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thank you:)

@QuncCccccc QuncCccccc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thank you:)!

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 13, 2026
@Mairramer Mairramer added the CICD Run CI/CD label Oct 13, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 13, 2026
@Mairramer Mairramer added the CICD Run CI/CD label Oct 13, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 13, 2026
@Mairramer Mairramer added the CICD Run CI/CD label Oct 13, 2026
@QuncCccccc QuncCccccc added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 13, 2026
@auto-submit
auto-submit Bot merged commit a0e7807 into flutter:main Oct 13, 2026
14 checks passed
opensource-fork-sync Bot pushed a commit to cla7aye15I4nd/opensource-flutter__flutter that referenced this pull request Oct 15, 2026
Roll Packages from a1ed23c to da40eaf (33 revisions)

flutter/packages@a1ed23c...da40eaf

2026-10-15 [email protected] [pigeon]add support for sealed classes
in native interop (flutter/packages#13175)
2026-10-15 [email protected] [pigeon] Replace full JNI bindings with
stubs for API classes that aren't needed (flutter/packages#13173)
2026-10-14 [email protected] [material_ui] Fix
self-comparison and assert bugs in semantics_tester
(flutter/packages#12722)
2026-10-14 [email protected] [camera_avfoundation] Replace the
deprecated 'videoOrientation' with 'videoRotationAngle' on iOS 17+
(flutter/packages#13247)
2026-10-14 [email protected] [material_ui] Fix GridTile crash when the
incoming constraints are unbounded (flutter/packages#12586)
2026-10-14 [email protected] [in_app_purchase_storekit] Remove unused
and deprecated "SKDownload" hosted content (flutter/packages#13222)
2026-10-14 [email protected]
[material_ui] Add supportingTextPadding to InputDecorator
(flutter/packages#12769)
2026-10-14 [email protected]
[vector_graphics_codec] Add support for encoding and decoding paint blur
commands (flutter/packages#13148)
2026-10-14 [email protected] [go_router] Fix redirect zone async error
handling (flutter/packages#12836)
2026-10-14 [email protected] Sync release-go_router-18.1.0 to
main (flutter/packages#13260)
2026-10-14 [email protected] Sync release-cupertino_ui-1.1.3 to
main (flutter/packages#13258)
2026-10-14 [email protected] [material_ui] add enableInlinePrediction
parameter to TextFormField (flutter/packages#12793)
2026-10-14 [email protected] Sync release-material_ui-1.7.0 to
main (flutter/packages#13259)
2026-10-14 [email protected] [material_ui] Add the
Material shapes. (flutter/packages#12771)
2026-10-14 [email protected] [material_ui]
Add tertiary tap callbacks to InkResponse and InkWell
(flutter/packages#12676)
2026-10-14 [email protected] Roll Flutter from
4249ccb to 426be96 (44 revisions) (flutter/packages#13256)
2026-10-14 [email protected] [material_ui]
fixes expanded state in the a11y_assessment home page dropdown
(flutter/packages#13219)
2026-10-14 [email protected] [VPAT] Fix semantics in
a11y_assessments drawer, radio, and rail demos (flutter/packages#13159)
2026-10-14 [email protected]
[google_maps_flutter] Add onPointOfInterestTap (flutter/packages#11872)
2026-10-14 [email protected] [go_router] Fix shell route key collisions
and dynamic branch updates (flutter/packages#12549)
2026-10-14 [email protected] Manual roll Flutter from
d03768e to 4249ccb (158 revisions) (flutter/packages#13220)
2026-10-14 [email protected] [image_picker_ios] Require Flutter
3.47 / iOS 15 and drop deprecated kUTType code paths
(flutter/packages#13224)
2026-10-13 [email protected] [in_app_purchase_storekit] Fixes
Storekit 2 flaky tests (flutter/packages#13213)
2026-10-13 [email protected] [local_auth] Convert Android to new
Pigeon suspend support (flutter/packages#12894)
2026-10-13 [email protected] [material_ui] Add
enabled parameter to DropdownButtonFormField (flutter/packages#12610)
2026-10-13 [email protected] [go_router] Link ShellRoute docs to
StatefulShellRoute (flutter/packages#13120)
2026-10-13 [email protected] FadeForwards page transition
performance (flutter/packages#13161)
2026-10-13 [email protected]
[in_app_purchase_android] Guard async billing callbacks against
duplicate listener responses (flutter/packages#12640)
2026-10-13 [email protected] [google_sign_in] Remove the
Objective-C exception catcher (flutter/packages#13156)
2026-10-13 [email protected] [camera_avfoundation] Remove availability
checks below the package minimum iOS 13 (flutter/packages#13214)
2026-10-13 [email protected] [google_maps_flutter_ios] Remove iOS 14
availability check from example UI tests (flutter/packages#13218)
2026-10-13 [email protected] [google_sign_in] Convert
implementation to Kotlin (flutter/packages#13005)
2026-10-13 [email protected] [image_picker_ios] PR 2/5 Add a
PHImageManager image-data seam and migrate plugin native tests to Swift
Testing (flutter/packages#13079)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
...
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
Roll Packages from a1ed23c to da40eaf (33 revisions)

flutter/packages@a1ed23c...da40eaf

2026-10-15 [email protected] [pigeon]add support for sealed classes
in native interop (flutter/packages#13175)
2026-10-15 [email protected] [pigeon] Replace full JNI bindings with
stubs for API classes that aren't needed (flutter/packages#13173)
2026-10-14 [email protected] [material_ui] Fix
self-comparison and assert bugs in semantics_tester
(flutter/packages#12722)
2026-10-14 [email protected] [camera_avfoundation] Replace the
deprecated 'videoOrientation' with 'videoRotationAngle' on iOS 17+
(flutter/packages#13247)
2026-10-14 [email protected] [material_ui] Fix GridTile crash when the
incoming constraints are unbounded (flutter/packages#12586)
2026-10-14 [email protected] [in_app_purchase_storekit] Remove unused
and deprecated "SKDownload" hosted content (flutter/packages#13222)
2026-10-14 [email protected]
[material_ui] Add supportingTextPadding to InputDecorator
(flutter/packages#12769)
2026-10-14 [email protected]
[vector_graphics_codec] Add support for encoding and decoding paint blur
commands (flutter/packages#13148)
2026-10-14 [email protected] [go_router] Fix redirect zone async error
handling (flutter/packages#12836)
2026-10-14 [email protected] Sync release-go_router-18.1.0 to
main (flutter/packages#13260)
2026-10-14 [email protected] Sync release-cupertino_ui-1.1.3 to
main (flutter/packages#13258)
2026-10-14 [email protected] [material_ui] add enableInlinePrediction
parameter to TextFormField (flutter/packages#12793)
2026-10-14 [email protected] Sync release-material_ui-1.7.0 to
main (flutter/packages#13259)
2026-10-14 [email protected] [material_ui] Add the
Material shapes. (flutter/packages#12771)
2026-10-14 [email protected] [material_ui]
Add tertiary tap callbacks to InkResponse and InkWell
(flutter/packages#12676)
2026-10-14 [email protected] Roll Flutter from
4249ccb to 426be96 (44 revisions) (flutter/packages#13256)
2026-10-14 [email protected] [material_ui]
fixes expanded state in the a11y_assessment home page dropdown
(flutter/packages#13219)
2026-10-14 [email protected] [VPAT] Fix semantics in
a11y_assessments drawer, radio, and rail demos (flutter/packages#13159)
2026-10-14 [email protected]
[google_maps_flutter] Add onPointOfInterestTap (flutter/packages#11872)
2026-10-14 [email protected] [go_router] Fix shell route key collisions
and dynamic branch updates (flutter/packages#12549)
2026-10-14 [email protected] Manual roll Flutter from
d03768e to 4249ccb (158 revisions) (flutter/packages#13220)
2026-10-14 [email protected] [image_picker_ios] Require Flutter
3.47 / iOS 15 and drop deprecated kUTType code paths
(flutter/packages#13224)
2026-10-13 [email protected] [in_app_purchase_storekit] Fixes
Storekit 2 flaky tests (flutter/packages#13213)
2026-10-13 [email protected] [local_auth] Convert Android to new
Pigeon suspend support (flutter/packages#12894)
2026-10-13 [email protected] [material_ui] Add
enabled parameter to DropdownButtonFormField (flutter/packages#12610)
2026-10-13 [email protected] [go_router] Link ShellRoute docs to
StatefulShellRoute (flutter/packages#13120)
2026-10-13 [email protected] FadeForwards page transition
performance (flutter/packages#13161)
2026-10-13 [email protected]
[in_app_purchase_android] Guard async billing callbacks against
duplicate listener responses (flutter/packages#12640)
2026-10-13 [email protected] [google_sign_in] Remove the
Objective-C exception catcher (flutter/packages#13156)
2026-10-13 [email protected] [camera_avfoundation] Remove availability
checks below the package minimum iOS 13 (flutter/packages#13214)
2026-10-13 [email protected] [google_maps_flutter_ios] Remove iOS 14
availability check from example UI tests (flutter/packages#13218)
2026-10-13 [email protected] [google_sign_in] Convert
implementation to Kotlin (flutter/packages#13005)
2026-10-13 [email protected] [image_picker_ios] PR 2/5 Add a
PHImageManager image-data seam and migrate plugin native tests to Swift
Testing (flutter/packages#13079)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC [email protected] on the revert to ensure that a
human
is aware of the problem.

To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: material_ui triage-design Should be looked at in design triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Why is DropdownButtonFormField's onChanged required?

3 participants