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

Flutter: PopupMenuButton verwenden und Menüstrukturen

Ahmet Balaman

Zuletzt aktualisiert:

5 Min. Lesezeit

FlutterPopupMenuButtonPopupMenuItemMenuAppBarUI
Flutter: PopupMenuButton verwenden und Menüstrukturen

Mit PopupMenuButton bietet man sekundäre Aktionen an, ohne Platz auf dem Bildschirm zu verbrauchen: Der Nutzer tippt auf das Drei-Punkte-Symbol, ein kleines Menü öffnet sich, ein Eintrag wird gewählt, und das Menü schließt sich wieder. Wir verwenden es für Aktionen, die nicht in die AppBar passen, etwa „Einstellungen, Hilfe, Abmelden“, oder für die Optionen „Bearbeiten, Teilen, Löschen“ einer Listenzeile. Jeder Eintrag trägt einen value, und über diesen Wert erfahren wir in onSelected, welcher Eintrag gewählt wurde.

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

PopupMenuButton<String>(
  onSelected: (String value) {
    debugPrint('Gewählt: $value');
  },
  itemBuilder: (BuildContext context) => const [
    PopupMenuItem<String>(
      value: 'edit',
      child: Text('Bearbeiten'),
    ),
    PopupMenuItem<String>(
      value: 'delete',
      child: Text('Löschen'),
    ),
  ],
)

Ohne icon oder child wird der Button mit dem „Mehr“-Symbol der jeweiligen Plattform gezeichnet. Am häufigsten sitzt er in der actions-Liste einer AppBar.

Verwendung mit Enum

Bei String-Werten rutscht ein Tippfehler ('delte' statt 'delete') am Compiler vorbei, und das Menü tut stillschweigend nichts. Ein Enum als Werttyp ist die gesündere Wahl:

enum MenuAction { edit, delete, share }

PopupMenuButton<MenuAction>(
  onSelected: (MenuAction action) {
    switch (action) {
      case MenuAction.edit:
        _editItem();
      case MenuAction.delete:
        _deleteItem();
      case MenuAction.share:
        _shareItem();
    }
  },
  itemBuilder: (context) => const [
    PopupMenuItem(value: MenuAction.edit, child: Text('Bearbeiten')),
    PopupMenuItem(value: MenuAction.delete, child: Text('Löschen')),
    PopupMenuItem(value: MenuAction.share, child: Text('Teilen')),
  ],
)

In Dart 3 braucht ein case mit Rumpf kein break mehr. Noch besser: Kommt ein neuer Wert ins Enum, erinnert der Compiler an das fehlende case. Was Enums sonst noch können, zeigt der Beitrag zu Enums in Dart.

PopupMenuItem<String>(
  value: 'settings',
  child: Row(
    children: const [
      Icon(Icons.settings),
      SizedBox(width: 10),
      Text('Einstellungen'),
    ],
  ),
)

PopupMenuDivider und CheckedPopupMenuItem

Eine Trennlinie gruppiert Einträge. Die gefährliche Aktion (Löschen) vom Rest abzusetzen ist eine gute Gewohnheit:

itemBuilder: (context) => const [
  PopupMenuItem(value: 'edit', child: Text('Bearbeiten')),
  PopupMenuItem(value: 'copy', child: Text('Kopieren')),
  PopupMenuDivider(), // Trennlinie
  PopupMenuItem(value: 'delete', child: Text('Löschen')),
]

Zeigt das Menü keine Aktion, sondern eine Einstellung (zum Beispiel die Sortierung), verwendet man CheckedPopupMenuItem, das die aktuelle Wahl mit einem Haken markiert:

CheckedPopupMenuItem(
  value: SortBy.date,
  checked: _sortBy == SortBy.date,
  child: const Text('Nach Datum'),
)

Wichtige Eigenschaften

Eigenschaft Beschreibung
onSelected Wird aufgerufen, wenn ein Eintrag gewählt wird
onCanceled Wird aufgerufen, wenn das Menü ohne Auswahl geschlossen wird
itemBuilder Erzeugt die Menüeinträge
icon Symbol des Menü-Buttons
child Eigenes Button-Widget
tooltip Text bei langem Druck und für Screenreader
position PopupMenuPosition.over oder under
offset Versatz der Menüposition
shape Form des Menüs
color Hintergrundfarbe des Menüs
enabled Menü aktiv oder inaktiv

Verwendung mit eigenem Button

PopupMenuButton<String>(
  position: PopupMenuPosition.under, // Menü unterhalb des Buttons öffnen
  onSelected: (value) {},
  itemBuilder: (context) => const [
    PopupMenuItem(value: 'week', child: Text('Diese Woche')),
    PopupMenuItem(value: 'month', child: Text('Dieser Monat')),
  ],
  child: Container(
    padding: const EdgeInsets.all(12),
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(8),
    ),
    child: Row(
      mainAxisSize: MainAxisSize.min,
      children: const [
        Text('Menü', style: TextStyle(color: Colors.white)),
        Icon(Icons.arrow_drop_down, color: Colors.white),
      ],
    ),
  ),
)

Wann verwenden – und wann nicht?

Ein Popup-Menü versteckt Aktionen. Das ist Stärke und Schwäche zugleich: Der Bildschirm wird ruhiger, aber der Nutzer erfährt womöglich nie, dass es die Aktion gibt.

  • Verwenden für sekundäre, selten genutzte Aktionen, für Optionen einer einzelnen Listenzeile und für alles jenseits des dritten Symbols in einer AppBar.
  • Nicht verwenden für die Hauptaktion eines Bildschirms. „Neue Notiz“ darf nicht im Menü verschwinden, sondern verdient einen sichtbaren Button oder einen FAB. Bei nur ein bis zwei Aktionen sind einfache IconButtons besser.
  • Nicht verwenden, um in einem Formular einen Wert auszuwählen. Muss der gewählte Wert sichtbar bleiben, ist der DropdownButton die richtige Komponente; ein Popup-Menü merkt sich keine Auswahl, es löst nur eine Aktion aus.
  • Nicht verwenden für viele oder erklärungsbedürftige Optionen. Auf dem Smartphone bietet showModalBottomSheet mehr Platz und liegt näher am Daumen (siehe BottomSheet-Beitrag). Wer auf dem Desktop Untermenüs braucht, findet in MenuAnchor aus Material 3 das passende Werkzeug.

Häufige Fehler

1. Eintrag ohne value

Symptom: Man tippt auf den Eintrag, das Menü schließt sich, aber onSelected wird nie aufgerufen.

Ursache: Ein Eintrag ohne value liefert null, und PopupMenuButton deutet das als „nichts ausgewählt“. Statt onSelected läuft onCanceled.

Lösung: Jedem PopupMenuItem einen value geben und den generischen Typ des Buttons ausschreiben (PopupMenuButton<MenuAction>).

2. Dialog oder Navigation in onTap

Symptom: Im Parameter onTap eines PopupMenuItem wird showDialog aufgerufen, und in manchen Flutter-Versionen blitzt der Dialog nur kurz auf und schließt sich sofort wieder.

Ursache: onTap läuft, während die Menü-Route geschlossen wird. In älteren Versionen nahm das pop, das das Menü schloss, den frisch geöffneten Dialog gleich mit.

Lösung: onTap nur für kleine Aufgaben ohne Context nutzen und Dialoge oder Seitenwechsel in onSelected erledigen. onSelected wird aufgerufen, nachdem die Menü-Route entfernt wurde, und ist damit unabhängig von der Version sicher.

3. child und icon gleichzeitig übergeben

Symptom: Die Assertion „You can only pass [child] or [icon], not both.“

Lösung: icon verwenden, wenn nur ein anderes Symbol gewünscht ist, und child, wenn man den ganzen Button selbst zeichnen will.

Mini-Szenario: Bearbeiten, Teilen, Löschen in einer Listenzeile

Stellen wir uns eine Notizliste vor. Jede Zeile endet mit einem Drei-Punkte-Menü; bei „Löschen“ wird zuerst nachgefragt und danach eine kurze Meldung gezeigt. Zuerst die Zeile:

enum NoteAction { edit, share, delete }

ListTile(
  title: Text(note.title),
  trailing: PopupMenuButton<NoteAction>(
    tooltip: 'Optionen zur Notiz',
    onSelected: (action) => _onNoteAction(action, note),
    itemBuilder: (context) => const [
      PopupMenuItem(value: NoteAction.edit, child: Text('Bearbeiten')),
      PopupMenuItem(value: NoteAction.share, child: Text('Teilen')),
      PopupMenuDivider(),
      PopupMenuItem(value: NoteAction.delete, child: Text('Löschen')),
    ],
  ),
)

Dann die Methode, die die Aktionen verarbeitet:

Future<void> _onNoteAction(NoteAction action, Note note) async {
  switch (action) {
    case NoteAction.edit:
      await Navigator.push(
        context,
        MaterialPageRoute(builder: (_) => EditNotePage(note: note)),
      );
    case NoteAction.share:
      // Teilen-Logik
      break;
    case NoteAction.delete:
      final ok = await showDialog<bool>(
        context: context,
        builder: (dialogContext) => AlertDialog(
          title: const Text('Notiz löschen?'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(dialogContext, false),
              child: const Text('Abbrechen'),
            ),
            FilledButton(
              onPressed: () => Navigator.pop(dialogContext, true),
              child: const Text('Löschen'),
            ),
          ],
        ),
      );
      if (ok != true || !mounted) return;
      setState(() => _notes.remove(note));
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Notiz gelöscht')),
      );
  }
}

Das Menü meldet nur die Absicht; die eigentliche Arbeit steckt in einer einzigen Methode. So lassen sich dieselben Aktionen morgen problemlos an eine Wischgeste oder einen Button auf der Detailseite hängen. Die Einzelheiten der Rückfrage stehen im Beitrag zum AlertDialog. Beachten Sie auch die mounted-Prüfung nach dem await: Der Nutzer könnte die Seite verlassen haben, während der Dialog offen war.

Häufig gestellte Fragen

Warum wird onSelected bei meinem PopupMenuButton nicht aufgerufen?

Sehr wahrscheinlich hat der gewählte PopupMenuItem keinen value. Ein Eintrag ohne Wert liefert null, und dann wird onCanceled statt onSelected aufgerufen.

Wie deaktiviere ich einen Menüeintrag?

Übergeben Sie dem PopupMenuItem enabled: false. Er wird blass dargestellt und lässt sich nicht antippen. Das ganze Menü deaktiviert der Parameter enabled des PopupMenuButton.

Wie öffne ich das Menü unterhalb des Buttons?

Standardmäßig öffnet sich das Menü über dem Button. Mit position: PopupMenuPosition.under erscheint es direkt darunter, und über offset lässt sich die Position feinjustieren.

Was ist der Unterschied zwischen PopupMenuButton und DropdownButton?

PopupMenuButton löst eine Aktion aus und speichert die Auswahl nicht. DropdownButton lässt einen Wert auswählen und zeigt ihn auf dem Button an – genau das, was Formulare brauchen.

Kommentare