From f8a1891d714ab44fa6058a261b8918216845f35b Mon Sep 17 00:00:00 2001 From: Ahmed Elsayed Date: Wed, 29 Apr 2026 22:04:57 +0300 Subject: [PATCH] Improve docs and validate selectedItemBuilder length --- README.md | 6 +-- packages/dropdown_button2/CHANGELOG.md | 1 + .../example/custom_dropdown_button2.dart | 2 +- .../lib/src/dropdown_button2.dart | 44 +++++++++---------- .../lib/src/dropdown_menu_item.dart | 6 +-- .../lib/src/dropdown_route.dart | 9 ++-- packages/dropdown_button2/lib/src/utils.dart | 2 +- packages/dropdown_button2/pubspec.yaml | 2 +- .../test/dropdown_button2_test.dart | 41 +++++++++++++++++ 9 files changed, 77 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f79b2895..1142349c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## Intro -Flutter's core Dropdown Button widget with steady dropdown menu and many other options you can +Flutter's core Dropdown Button widget with a steady dropdown menu and many other options you can customize to your needs. Image @@ -68,7 +68,7 @@ customize to your needs. | Option | Description | Type | Required | | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------- | :------: | | [items](https://pub.dev/documentation/dropdown_button2/latest/dropdown_button2/DropdownButton2/items.html) | The list of items the user can select | List> | Yes | -| [selectedItemBuilder](https://pub.dev/documentation/dropdown_button2/latest/dropdown_button2/DropdownButton2/selectedItemBuilder.html) | A builder to customize how the selected item will be displayed on the button | DropdownButtonBuilder | No | +| [selectedItemBuilder](https://pub.dev/documentation/dropdown_button2/latest/dropdown_button2/DropdownButton2/selectedItemBuilder.html) | A builder to customize how the selected item will be displayed on the button | DropdownButton2Builder | No | | [valueListenable](https://pub.dev/documentation/dropdown_button2/latest/dropdown_button2/DropdownButton2/valueListenable.html) | A [ValueListenable] that represents the value of the currently selected [DropdownItem]. | ValueListenable? | No | | [multiValueListenable](https://pub.dev/documentation/dropdown_button2/latest/dropdown_button2/DropdownButton2/multiValueListenable.html) | A [ValueListenable] that represents a list of the currently selected [DropdownItem]s | ValueListenable>? | No | | [hint](https://pub.dev/documentation/dropdown_button2/latest/dropdown_button2/DropdownButton2/hint.html) | The placeholder displayed before the user choose an item | Widget | No | @@ -1079,7 +1079,7 @@ class CustomDropdownButton2 extends StatelessWidget { final ValueListenable? valueListenable; final List dropdownItems; final ValueChanged? onChanged; - final DropdownButtonBuilder? selectedItemBuilder; + final DropdownButton2Builder? selectedItemBuilder; final Alignment? hintAlignment; final Alignment? valueAlignment; final double? buttonHeight, buttonWidth; diff --git a/packages/dropdown_button2/CHANGELOG.md b/packages/dropdown_button2/CHANGELOG.md index e1c5e1b4..d1ef4ecd 100644 --- a/packages/dropdown_button2/CHANGELOG.md +++ b/packages/dropdown_button2/CHANGELOG.md @@ -10,6 +10,7 @@ - Add `barrierBlocksInteraction` to allow interaction with underlying widgets while the dropdown menu is open. - Properly dispose internal FocusNode when replaced by an external FocusNode. - Add `mouseCursor` to the dropdown button and MenuItemStyleData to customize the mouse cursor when hovering, closes #416. +- Improve docs and validate selectedItemBuilder length. ## 3.0.0 diff --git a/packages/dropdown_button2/example/custom_dropdown_button2.dart b/packages/dropdown_button2/example/custom_dropdown_button2.dart index e068471d..2005e9fc 100644 --- a/packages/dropdown_button2/example/custom_dropdown_button2.dart +++ b/packages/dropdown_button2/example/custom_dropdown_button2.dart @@ -37,7 +37,7 @@ class CustomDropdownButton2 extends StatelessWidget { final ValueListenable? valueListenable; final List dropdownItems; final ValueChanged? onChanged; - final DropdownButtonBuilder? selectedItemBuilder; + final DropdownButton2Builder? selectedItemBuilder; final Alignment? hintAlignment; final Alignment? valueAlignment; final double? buttonHeight, buttonWidth; diff --git a/packages/dropdown_button2/lib/src/dropdown_button2.dart b/packages/dropdown_button2/lib/src/dropdown_button2.dart index 57f696da..3e9122a9 100644 --- a/packages/dropdown_button2/lib/src/dropdown_button2.dart +++ b/packages/dropdown_button2/lib/src/dropdown_button2.dart @@ -34,6 +34,9 @@ const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero; /// A builder to customize dropdown buttons. /// /// Used by [DropdownButton2.selectedItemBuilder]. +/// +/// The list of widgets returned by this builder must be exactly the same length +/// as the [DropdownButton2.items] list. typedef DropdownButton2Builder = Iterable Function(BuildContext context); /// A builder to customize the selected menu item. @@ -83,7 +86,7 @@ typedef SearchMatchFn = bool Function(DropdownItem item, String searchValu /// * class DropdownButton2 extends StatefulWidget { /// Creates a DropdownButton2. - /// It's customizable DropdownButton with steady dropdown menu and many other features. + /// It's a customizable dropdown button with a steady dropdown menu and many other features. /// /// The [items] must have distinct values. If [valueListenable] isn't null then its value /// must be equal to one of the [DropdownItem] values. If [multiValueListenable] isn't null @@ -192,14 +195,8 @@ class DropdownButton2 extends StatefulWidget { /// /// When a [DropdownItem] is selected, the widget that will be displayed /// from the list corresponds to the [DropdownItem] of the same index - /// in [items]. - /// - /// {@tool dartpad} - /// This sample shows a [DropdownButton] with a button with [Text] that - /// corresponds to but is unique from [DropdownItem]. - /// - /// ** See code in examples/api/lib/material/dropdown/dropdown_button.selected_item_builder.0.dart ** - /// {@end-tool} + /// in [items]. The list of widgets returned by this builder must be exactly + /// the same length as the [items] list. /// /// If this callback is null, the [DropdownItem] from [items] /// that matches the selected [DropdownItem]'s value will be displayed. @@ -263,13 +260,6 @@ class DropdownButton2 extends StatefulWidget { /// To use a separate text style for selected item when it's displayed within /// the dropdown button, consider using [selectedItemBuilder]. /// - /// {@tool dartpad} - /// This sample shows a `DropdownButton` with a dropdown button text style - /// that is different than its menu items. - /// - /// ** See code in examples/api/lib/material/dropdown/dropdown_button.style.0.dart ** - /// {@end-tool} - /// /// Defaults to the [TextTheme.titleMedium] value of the current /// [ThemeData.textTheme] of the current [Theme]. final TextStyle? style; @@ -450,8 +440,8 @@ class _DropdownButton2State extends State> with WidgetsBin final GlobalKey> _buttonRectKey = GlobalKey(); - // Using ValueNotifier for the Rect of DropdownButton so the dropdown menu listen and - // update its position if DropdownButton's position has changed, as when keyboard open. + // Using ValueNotifier for the Rect of DropdownButton2 so the dropdown menu listen and + // update its position if DropdownButton2's position has changed, as when keyboard open. final ValueNotifier _buttonRect = ValueNotifier(null); // Ancestor scroll positions we listen to while the menu is open, so the menu @@ -850,9 +840,19 @@ class _DropdownButton2State extends State> with WidgetsBin // We should explicitly type the items list to be a list of , // otherwise, no explicit type adding items maybe trigger a crash/failure // when hint and selectedItemBuilder are provided. - final buttonItems = widget.selectedItemBuilder == null - ? (widget.items != null ? List.of(widget.items!) : []) - : List.of(widget.selectedItemBuilder!(context)); + final List buttonItems; + if (widget.selectedItemBuilder != null) { + final selectedItems = List.of(widget.selectedItemBuilder!(context)); + assert( + widget.items == null || selectedItems.length == widget.items!.length, + 'The selectedItemBuilder must return a list of widgets with the same length as the items list.\n' + 'Currently, selectedItemBuilder returns a list of length ${selectedItems.length}, ' + 'but items has length ${widget.items!.length}.', + ); + buttonItems = selectedItems; + } else { + buttonItems = widget.items != null ? List.of(widget.items!) : []; + } int? hintIndex; if (widget.hint != null || (!_enabled && widget.disabledHint != null)) { @@ -1189,7 +1189,7 @@ class DropdownButtonFormField2 extends FormField { ? errorBuilder(state.context, field.errorText!) : null; final String? errorText = error == null ? field.errorText : null; - // Clear the decoration hintText because DropdownButton has its own hint logic. + // Clear the decoration hintText because DropdownButton2 has its own hint logic. final String? hintText = effectiveDecoration.hintText != null ? '' : null; effectiveDecoration = effectiveDecoration.copyWith( diff --git a/packages/dropdown_button2/lib/src/dropdown_menu_item.dart b/packages/dropdown_button2/lib/src/dropdown_menu_item.dart index 9b6a19f1..8fae4940 100644 --- a/packages/dropdown_button2/lib/src/dropdown_menu_item.dart +++ b/packages/dropdown_button2/lib/src/dropdown_menu_item.dart @@ -25,7 +25,7 @@ class DropdownItem extends _DropdownMenuItemContainer { /// The value to return if the user selects this menu item. /// - /// Eventually returned in a call to [DropdownButton.onChanged]. + /// Eventually returned in a call to [DropdownButton2.onChanged]. final T? value; /// Whether or not a user can select this menu item. @@ -63,8 +63,8 @@ class DropdownItem extends _DropdownMenuItemContainer { } // The container widget for a menu item created by a [DropdownButton2]. It -// provides the default configuration for [DropdownMenuItem]s, as well as a -// [DropdownButton]'s hint and disabledHint widgets. +// provides the default configuration for [DropdownItem]s, as well as a +// [DropdownButton2]'s hint and disabledHint widgets. class _DropdownMenuItemContainer extends StatelessWidget { /// Creates an item for a dropdown menu. /// diff --git a/packages/dropdown_button2/lib/src/dropdown_route.dart b/packages/dropdown_button2/lib/src/dropdown_route.dart index 9259eb8c..eaca43fd 100644 --- a/packages/dropdown_button2/lib/src/dropdown_route.dart +++ b/packages/dropdown_button2/lib/src/dropdown_route.dart @@ -371,11 +371,10 @@ class _DropdownRoutePageState extends State<_DropdownRoutePage> { void initState() { super.initState(); // Computing the initialScrollOffset now, before the items have been laid - // out. This only works if the item heights are effectively fixed, i.e. either - // DropdownButton.itemHeight is specified or DropdownButton.itemHeight is null - // and all of the items' intrinsic heights are less than _kMenuItemHeight. - // Otherwise the initialScrollOffset is just a rough approximation based on - // treating the items as if their heights were all equal to _kMenuItemHeight. + // out. This is accurate only if no item sets [DropdownItem.intrinsicHeight] + // to true. When an item uses an intrinsic height, the offset is just a + // rough approximation that uses the declared [DropdownItem.height] as a + // fallback, since the actual height can't be known until layout. final _MenuLimits menuLimits = widget.route.getMenuLimits( widget.buttonRect, widget.constraints.maxHeight, diff --git a/packages/dropdown_button2/lib/src/utils.dart b/packages/dropdown_button2/lib/src/utils.dart index 36c3146e..5744d919 100644 --- a/packages/dropdown_button2/lib/src/utils.dart +++ b/packages/dropdown_button2/lib/src/utils.dart @@ -10,7 +10,7 @@ void _uniqueValueAssert( } String assertMessage(T value) { - return "There should be exactly one item with [DropdownButton]'s value: " + return "There should be exactly one item with [DropdownButton2]'s value: " '$value. \n' 'Either zero or 2 or more [DropdownItem]s were detected ' 'with the same value'; diff --git a/packages/dropdown_button2/pubspec.yaml b/packages/dropdown_button2/pubspec.yaml index 8d58f696..094d125e 100644 --- a/packages/dropdown_button2/pubspec.yaml +++ b/packages/dropdown_button2/pubspec.yaml @@ -1,5 +1,5 @@ name: dropdown_button2 -description: Flutter's core Dropdown Button widget with steady dropdown menu and many options you can customize to your needs. +description: Flutter's core Dropdown Button widget with a steady dropdown menu and many other options you can customize to your needs. version: 3.0.0 repository: https://github.com/AhmedLSayed9/dropdown_button2 issue_tracker: https://github.com/AhmedLSayed9/dropdown_button2/issues diff --git a/packages/dropdown_button2/test/dropdown_button2_test.dart b/packages/dropdown_button2/test/dropdown_button2_test.dart index 35995ec8..17d69cfb 100644 --- a/packages/dropdown_button2/test/dropdown_button2_test.dart +++ b/packages/dropdown_button2/test/dropdown_button2_test.dart @@ -1053,4 +1053,45 @@ void main() { }, ); }); + + group( + 'Selected Item Builder', + () { + final menuItems = List.generate(4, (int index) => index); + + List> buildItems() { + return menuItems.map>((int item) { + return DropdownItem( + value: item, + child: Text(item.toString()), + ); + }).toList(); + } + + testWidgets( + 'selectedItemBuilder should assert when it returns a different number of widgets than items', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownButton2( + items: buildItems(), + selectedItemBuilder: (BuildContext context) => const [Text('only one')], + onChanged: (_) {}, + ), + ), + ), + ); + + final Object? exception = tester.takeException(); + expect(exception, isA()); + expect( + (exception! as AssertionError).message, + 'The selectedItemBuilder must return a list of widgets with the same length as the items list.\n' + 'Currently, selectedItemBuilder returns a list of length 1, but items has length 4.', + ); + }, + ); + }, + ); }