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

Flutter: DropdownButton verwenden und Eigenschaften

Ahmet Balaman

Zuletzt aktualisiert:

6 Min. Lesezeit

FlutterDropdownButtonDropdownMenuDropdownButtonFormFieldFormMaterial 3
Flutter: DropdownButton verwenden und Eigenschaften

DropdownButton ist die platzsparendste Möglichkeit, den Nutzer genau einen Wert aus einer festen Liste wählen zu lassen; Android-Entwickler kennen das als Spinner. Geschlossen zeigt er nur den gewählten Wert, ein Tipp darauf öffnet die Liste. Wir verwenden ihn für Felder, deren Optionen vorab feststehen, etwa Kategorie, Stadt oder Währung. Weil kein Freitext möglich ist, verhindert er fehlerhafte Eingaben von vornherein.

Live-Demo

Sie können dieses Widget im interaktiven Beispiel unten ausprobieren:

💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.

Grundlegende Verwendung

DropdownButton hält keinen eigenen Zustand. Den gewählten Wert speichern wir selbst in einer Variablen und aktualisieren ihn in onChanged mit setState:

String? selectedValue;

DropdownButton<String>(
  value: selectedValue,
  hint: const Text('Bitte wählen'),
  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 muss ein Feld der State-Klasse sein und keine lokale Variable in build. Sonst wird es bei jedem Neuaufbau wieder null, und die Auswahl erscheint nie. Solange der Wert null ist, wird hint angezeigt; ein künstlicher Eintrag „Bitte wählen“ in der Liste ist also überflüssig.

Wichtige Eigenschaften

Eigenschaft Beschreibung
value Der ausgewählte Wert
hint Widget, das ohne Auswahl angezeigt wird
disabledHint Widget, das bei deaktiviertem Button angezeigt wird
items Liste der DropdownMenuItem
onChanged Wird bei einer Änderung aufgerufen; null deaktiviert den Button
isExpanded Passt die Breite an das übergeordnete Widget an
underline Widget für die Unterstreichung
icon Symbol des Aufklapp-Pfeils
dropdownColor Hintergrundfarbe der aufgeklappten Liste
menuMaxHeight Maximale Höhe der aufgeklappten Liste

Innerhalb von Formularen sollte DropdownButtonFormField bevorzugt werden. Es nimmt eine InputDecoration entgegen, sieht also aus wie die TextFormFields daneben, und nimmt über validator an der Validierung teil:

DropdownButtonFormField<String>(
  value: selectedValue,
  decoration: const InputDecoration(
    labelText: 'Kategorie',
    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 'Bitte wählen Sie eine Kategorie';
    }
    return null;
  },
)

Wie die Validierung über einen GlobalKey<FormState> ausgelöst wird, erklärt der Beitrag zum Form-Widget. Ein kleiner Hinweis: Neuere Flutter-Versionen empfehlen für den Parameter value dieses Widgets stattdessen initialValue. Zeigt der Editor eine Deprecation-Warnung, genügt es, den Parameter umzubenennen.

Angepasste DropdownMenuItem

Da child ein beliebiges Widget sein darf, lassen sich Einträge mit Symbolen und Labels anreichern:

DropdownMenuItem<String>(
  value: 'premium',
  child: Row(
    children: [
      const Icon(Icons.star, color: Colors.amber),
      const SizedBox(width: 10),
      const Text('Premium-Mitgliedschaft'),
      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(
          'Empfohlen',
          style: TextStyle(color: Colors.white, fontSize: 10),
        ),
      ),
    ],
  ),
)

Die Material-3-Alternative: DropdownMenu

Das mit Material 3 eingeführte DropdownMenu erledigt dieselbe Aufgabe im Look eines Textfelds. Der größte Unterschied: Man kann durch Tippen filtern.

DropdownMenu<String>(
  label: const Text('Land'),
  initialSelection: 'tr',
  enableFilter: true,
  requestFocusOnTap: true, // Tastatur auch auf dem Smartphone öffnen
  onSelected: (String? value) {
    setState(() => _country = value);
  },
  dropdownMenuEntries: const [
    DropdownMenuEntry(value: 'tr', label: 'Türkei'),
    DropdownMenuEntry(value: 'de', label: 'Deutschland'),
    DropdownMenuEntry(value: 'gb', label: 'Vereinigtes Königreich'),
  ],
)

Die Einträge sind hier keine Widgets, sondern DropdownMenuEntry-Objekte mit einem label-Text, und gefiltert wird über diesen Text. Sobald es mehr als etwa zehn Optionen gibt und Nutzer lieber tippen als scrollen, bietet DropdownMenu das bessere Erlebnis. Für ein klassisches Formularfeld mit fünf oder sechs Optionen bleibt DropdownButtonFormField die praktischste Lösung.

Wann verwenden – und wann nicht?

Ein Dropdown versteckt seine Optionen; der Nutzer muss einmal tippen, nur um zu sehen, was es gibt. An diesem Preis sollte man die Entscheidung ausrichten:

  • Verwenden bei ungefähr 4 bis 15 sich gegenseitig ausschließenden Optionen und in Formularen mit wenig Platz.
  • Nicht verwenden bei zwei oder drei Optionen. Ein SegmentedButton oder RadioListTile zeigt alle auf einmal und ist schneller und klarer. Solche Eingabekomponenten behandelt der Beitrag zu Input-Widgets.
  • Nicht verwenden bei sehr langen Listen. Durch Hunderte Einträge zu scrollen ist eine Qual; besser ist ein filterbares DropdownMenu oder eine eigene Auswahlseite mit Suchfeld.
  • Nicht verwenden für Mehrfachauswahl. Ein Dropdown hält einen Wert; für mehrere eignen sich FilterChips oder eine Liste mit Checkboxen.
  • Nicht verwenden, um Aktionen auszulösen. Befehle wie „Bearbeiten, Löschen“ sind Aktionen und keine Werte; sie gehören in einen PopupMenuButton.

Häufige Fehler

1. Der value ist nicht in den items enthalten

Symptom: Eine der bekanntesten Assertions in Flutter: „There should be exactly one item with [DropdownButton]'s value: … Either zero or 2 or more [DropdownMenuItem]s were detected with the same value“.

Die Ursache tritt in drei Varianten auf. Der Anfangswert steht nicht in der Liste (etwa value: 'Bitte wählen'). Die Liste hat sich später geändert, und die alte Auswahl ist nicht mehr enthalten. Oder zwei Einträge haben denselben value.

Lösung: Mit null starten und hint verwenden; die Auswahl zurücksetzen, sobald sich die Liste ändert; auf eindeutige Werte achten.

2. Eigene Klasse als value ohne ==

Symptom: Dieselbe Assertion, obwohl die Liste korrekt aussieht. Meist tritt sie auf, nachdem die Daten erneut von der API geladen wurden.

Ursache: In Dart sind zwei Objekte standardmäßig nicht gleich, auch wenn ihr Inhalt übereinstimmt. Das City(34, 'Istanbul') aus der neu geladenen Liste passt nicht zu dem alten Objekt, das wir noch halten.

Lösung: == und hashCode in der Klasse überschreiben oder gleich die id als value verwenden:

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. Lange Texte laufen über

Symptom: Bei langem ausgewähltem Text erscheint „A RenderFlex overflowed by … pixels on the right“ samt gelb-schwarzer Streifen.

Lösung: isExpanded: true setzen und dem Text des Eintrags overflow: TextOverflow.ellipsis geben. Sitzt das Dropdown in einer Row, reicht isExpanded allein nicht, weil eine Row ihrem Kind unbegrenzte Breite gibt; das Dropdown muss zusätzlich in ein Expanded gepackt werden.

4. Das Dropdown ist grau und nicht antippbar

Symptom: Der Button wird blass gezeichnet, öffnet sich nicht und meldet keinen Fehler.

Ursache: onChanged ist null oder die items-Liste ist leer. Flutter betrachtet den Button dann als deaktiviert.

Lösung: War das keine Absicht, onChanged angeben. War es Absicht, dem Nutzer mit disabledHint sagen, warum.

Mini-Szenario: Stadtteile abhängig von der Stadt

Stellen wir uns ein Adressformular vor: Zuerst wird die Stadt gewählt, und die Liste der Stadtteile füllt sich passend dazu. Genau hier passiert Fehler Nummer eins am häufigsten. Nach der Wahl von Berlin und Kreuzberg stellt der Nutzer die Stadt auf Hamburg um; das Stadtteil-Feld hat noch den value 'Kreuzberg', aber in der neuen Liste gibt es diesen Eintrag nicht.

final _formKey = GlobalKey<FormState>();

final Map<String, List<String>> _districts = {
  'Berlin': ['Kreuzberg', 'Mitte', 'Pankow'],
  'Hamburg': ['Altona', 'Eimsbüttel'],
  'München': ['Schwabing', 'Sendling'],
};

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: 'Stadt'),
          items: _districts.keys
              .map((c) => DropdownMenuItem(value: c, child: Text(c)))
              .toList(),
          onChanged: (value) => setState(() {
            _city = value;
            _district = null; // Stadt geändert, alter Stadtteil ungültig
          }),
          validator: (v) => v == null ? 'Stadt wählen' : null,
        ),
        const SizedBox(height: 16),
        DropdownButtonFormField<String>(
          key: ValueKey(_city), // Feld pro Stadt komplett neu aufbauen
          value: _district,
          isExpanded: true,
          decoration: const InputDecoration(labelText: 'Stadtteil'),
          disabledHint: const Text('Zuerst Stadt wählen'),
          items: districtList
              .map((d) => DropdownMenuItem(value: d, child: Text(d)))
              .toList(),
          onChanged: _city == null
              ? null
              : (value) => setState(() => _district = value),
          validator: (v) => v == null ? 'Stadtteil wählen' : null,
        ),
        const SizedBox(height: 24),
        FilledButton(
          onPressed: () {
            if (_formKey.currentState!.validate()) {
              // _city und _district können gespeichert werden
            }
          },
          child: const Text('Speichern'),
        ),
      ],
    ),
  );
}

Zwei Details lösen das Problem. Mit _district = null beim Wechsel der Stadt wird der ungültige Wert entfernt. Der key: ValueKey(_city) am Stadtteil-Feld sorgt dafür, dass das Formularfeld auch den intern gehaltenen alten Wert verwirft: Wechselt die Stadt, behandelt Flutter das Feld als neues Widget und baut es von Grund auf neu. Solange keine Stadt gewählt ist, bleibt das Stadtteil-Feld durch onChanged: null deaktiviert, und disabledHint sagt dem Nutzer, was zu tun ist.

Häufig gestellte Fragen

Wie setze ich beim DropdownButton einen Anfangswert?

Übergeben Sie dem Parameter value den Wert eines Eintrags aus items. Soll anfangs nichts ausgewählt sein, bleibt value null, und hint zeigt einen Hinweistext.

Warum erscheint „There should be exactly one item with [DropdownButton]'s value“?

Der übergebene value passt zu keinem oder zu mehr als einem Eintrag in items. Prüfen Sie den Anfangswert, ob die Auswahl nach einer Listenänderung zurückgesetzt wird und ob eigene Klassen == überschreiben.

Was ist der Unterschied zwischen DropdownButton und DropdownMenu?

DropdownButton nimmt Widgets als Einträge und verhält sich wie eine klassische Aufklappliste. Das DropdownMenu aus Material 3 sieht aus wie ein Textfeld, verwendet DropdownMenuEntry und erlaubt mit enableFilter das Filtern durch Tippen.

Wie bringe ich ein Dropdown auf volle Breite?

Setzen Sie isExpanded: true; das Dropdown füllt dann die Breite des Eltern-Widgets, und der Pfeil rückt an den rechten Rand. DropdownButtonFormField besitzt denselben Parameter.

Kommentare