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.
@@ -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.',
+ );
+ },
+ );
+ },
+ );
}