İçeriğe geç / Skip to content / Zum Inhalt

Flutter: DropdownButton Usage and Features

Ahmet Balaman

Last updated:

6 min read

FlutterDropdownButtonDropdownMenuDropdownButtonFormFieldFormMaterial 3
Flutter: DropdownButton Usage and Features

DropdownButton is the most compact way to let the user pick one value from a fixed list; Android developers know it as a Spinner. Closed, it shows only the selected value, and tapping it opens the list. We use it for fields whose options are known in advance, such as category, city or currency. Since the user cannot type free text, it also prevents invalid input from the start.

Live Demo

You can try this widget in the interactive example below:

💡 If the example above doesn't load, click DartPad to run it in a new tab.

Basic Usage

DropdownButton does not hold its own state. We keep the selected value in a variable and update it with setState inside onChanged:

String? selectedValue;

DropdownButton<String>(
  value: selectedValue,
  hint: const Text('Make a selection'),
  items: ['Option 1', 'Option 2', 'Option 3'].map((String value) {
    return DropdownMenuItem<String>(
      value: value,
      child: Text(value),
    );
  }).toList(),
  onChanged: (String? newValue) {
    setState(() {
      selectedValue = newValue;
    });
  },
)

selectedValue has to be a field of the State class, not a local variable inside build. Otherwise it is reset to null on every rebuild and the selection never shows. While the value is null the hint is displayed, so there is no need to add a fake "Please select" item to the list.

Important Properties

Property Description
value The selected value
hint Widget shown when no selection is made
disabledHint Widget shown while the button is disabled
items List of DropdownMenuItem
onChanged Called when the selection changes; null disables the button
isExpanded Adjusts width according to parent
underline Underline widget
icon Dropdown arrow icon
dropdownColor Background color of the dropdown list
menuMaxHeight Maximum height of the opened list

Inside a form, prefer DropdownButtonFormField. It takes an InputDecoration, so it looks like your TextFormFields, and it takes part in validation through validator:

DropdownButtonFormField<String>(
  value: selectedValue,
  decoration: const InputDecoration(
    labelText: 'Category',
    border: OutlineInputBorder(),
    prefixIcon: Icon(Icons.category),
  ),
  items: categories.map((String category) {
    return DropdownMenuItem<String>(
      value: category,
      child: Text(category),
    );
  }).toList(),
  onChanged: (String? newValue) {
    setState(() {
      selectedValue = newValue;
    });
  },
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please select a category';
    }
    return null;
  },
)

How validation is triggered through a GlobalKey<FormState> is explained in the Form post. A small note: recent Flutter versions recommend initialValue in place of this widget's value parameter. If your editor shows a deprecation warning, renaming the parameter is all it takes.

Customized DropdownMenuItem

Because child can be any widget, items can be enriched with icons and badges:

DropdownMenuItem<String>(
  value: 'premium',
  child: Row(
    children: [
      const Icon(Icons.star, color: Colors.amber),
      const SizedBox(width: 10),
      const Text('Premium Membership'),
      const SizedBox(width: 10),
      Container(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
        decoration: BoxDecoration(
          color: Colors.green,
          borderRadius: BorderRadius.circular(10),
        ),
        child: const Text(
          'Recommended',
          style: TextStyle(color: Colors.white, fontSize: 10),
        ),
      ),
    ],
  ),
)

The Material 3 Alternative: DropdownMenu

DropdownMenu, which arrived with Material 3, does the same job with the look of a text field, and its biggest difference is filtering by typing:

DropdownMenu<String>(
  label: const Text('Country'),
  initialSelection: 'tr',
  enableFilter: true,
  requestFocusOnTap: true, // Open the keyboard on mobile as well
  onSelected: (String? value) {
    setState(() => _country = value);
  },
  dropdownMenuEntries: const [
    DropdownMenuEntry(value: 'tr', label: 'Türkiye'),
    DropdownMenuEntry(value: 'de', label: 'Germany'),
    DropdownMenuEntry(value: 'gb', label: 'United Kingdom'),
  ],
)

The items here are not widgets but DropdownMenuEntry objects carrying a label text, and filtering works on that text. Once there are more than ten or so options and the user would rather type what they are looking for, DropdownMenu gives a better experience. For a classic form field with five or six options, DropdownButtonFormField is still the most practical choice.

When to Use It and When Not To

A dropdown hides its options; the user has to tap once just to see what is available. Decide with that cost in mind:

  • Use it for roughly 4 to 15 mutually exclusive options, and in forms where screen space is tight.
  • Don't use it for two or three options. A SegmentedButton or RadioListTile shows them all at once and is both faster and clearer. Input components like these are covered in the input widgets post.
  • Don't use it for very long lists. Scrolling through hundreds of items is painful; use a filterable DropdownMenu or a separate selection page with a search box.
  • Don't use it for multiple selection. A dropdown holds one value; for several, use FilterChips or a list with checkboxes.
  • Don't use it to trigger actions. Commands like "Edit, Delete" are actions, not values, and they belong in a PopupMenuButton.

Common Mistakes

1. The value is not among the items

Symptom: one of Flutter's best-known assertions: "There should be exactly one item with [DropdownButton]'s value: … Either zero or 2 or more [DropdownMenuItem]s were detected with the same value".

The cause comes in three flavours. The initial value is not in the list (something like value: 'Please select'). The list changed later and the old selection is no longer in it. Or two items share the same value.

Fix: start with null and use hint; reset the selection whenever the list changes; make sure the values are unique.

2. Using your own class as value without overriding ==

Symptom: the same assertion, even though the list looks correct. It typically appears after the data has been fetched from the API again.

Cause: in Dart, two objects are not equal by default even if their contents match. The City(34, 'Istanbul') in the freshly fetched list does not match the old object you are still holding.

Fix: override == and hashCode in the class, or simply use the id as the value:

class City {
  const City(this.id, this.name);
  final int id;
  final String name;

  @override
  bool operator ==(Object other) => other is City && other.id == id;

  @override
  int get hashCode => id.hashCode;
}

3. Long text overflowing

Symptom: with a long selected text you get "A RenderFlex overflowed by … pixels on the right" and the yellow and black stripes.

Fix: set isExpanded: true and add overflow: TextOverflow.ellipsis to the item text. If the dropdown sits inside a Row, isExpanded alone is not enough because a Row gives its child unbounded width; wrap the dropdown in an Expanded as well.

4. The dropdown looks gray and cannot be tapped

Symptom: the button is drawn faded, does not open on tap and throws no error.

Cause: onChanged is null or the items list is empty. Flutter treats the button as disabled in that case.

Fix: if you did not mean to disable it, provide onChanged. If you did, tell the user why with disabledHint.

Mini Scenario: Districts That Depend on the City

Picture an address form: the user picks a city first and the district list is filled according to that city. This is where mistake number one happens most. After choosing Istanbul and Kadıköy, the user switches the city to Ankara; the district field still has the value 'Kadıköy', but the new list contains no such item.

final _formKey = GlobalKey<FormState>();

final Map<String, List<String>> _districts = {
  'Istanbul': ['Kadıköy', 'Beşiktaş', 'Üsküdar'],
  'Ankara': ['Çankaya', 'Keçiören'],
  'Izmir': ['Konak', 'Bornova'],
};

String? _city;
String? _district;

@override
Widget build(BuildContext context) {
  final districtList = _districts[_city] ?? const <String>[];

  return Form(
    key: _formKey,
    child: Column(
      children: [
        DropdownButtonFormField<String>(
          value: _city,
          isExpanded: true,
          decoration: const InputDecoration(labelText: 'City'),
          items: _districts.keys
              .map((c) => DropdownMenuItem(value: c, child: Text(c)))
              .toList(),
          onChanged: (value) => setState(() {
            _city = value;
            _district = null; // City changed, the old district is invalid
          }),
          validator: (v) => v == null ? 'Select a city' : null,
        ),
        const SizedBox(height: 16),
        DropdownButtonFormField<String>(
          key: ValueKey(_city), // Rebuild the field from scratch per city
          value: _district,
          isExpanded: true,
          decoration: const InputDecoration(labelText: 'District'),
          disabledHint: const Text('Select a city first'),
          items: districtList
              .map((d) => DropdownMenuItem(value: d, child: Text(d)))
              .toList(),
          onChanged: _city == null
              ? null
              : (value) => setState(() => _district = value),
          validator: (v) => v == null ? 'Select a district' : null,
        ),
        const SizedBox(height: 24),
        FilledButton(
          onPressed: () {
            if (_formKey.currentState!.validate()) {
              // _city and _district are ready to be saved
            }
          },
          child: const Text('Save'),
        ),
      ],
    ),
  );
}

Two details make it work. Setting _district = null when the city changes clears the invalid value. The key: ValueKey(_city) on the district field makes the form field drop the old value it keeps internally as well: when the city changes, Flutter treats the field as a new widget and builds it from scratch. Until a city is chosen, onChanged: null keeps the district field disabled and disabledHint tells the user what to do.

Frequently Asked Questions

How do I set an initial value on a DropdownButton?

Pass the value of one of the entries in items to the value parameter. If nothing should be selected at first, leave value as null and show a prompt with hint.

Why do I get "There should be exactly one item with [DropdownButton]'s value"?

The given value matches none of the entries in items, or more than one. Check the initial value, whether the selection is reset after the list changes, and whether custom classes override ==.

What is the difference between DropdownButton and DropdownMenu?

DropdownButton takes widgets as items and behaves like a classic drop-down list. The Material 3 DropdownMenu looks like a text field, uses DropdownMenuEntry and allows filtering by typing through enableFilter.

How do I make a dropdown fill the full width?

Set isExpanded: true; the dropdown then fills its parent's width and the arrow icon moves to the right edge. DropdownButtonFormField has the same parameter.

Comments