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

Flutter: AlertDialog verwenden und anpassen

Ahmet Balaman

Zuletzt aktualisiert:

6 Min. Lesezeit

FlutterAlertDialogshowDialogDialogMaterial 3UI
Flutter: AlertDialog verwenden und anpassen

AlertDialog ist das modale Fenster, das sich in der Bildschirmmitte öffnet, wenn der Nutzer eine Entscheidung treffen muss. Solange es offen ist, reagiert der Bildschirm dahinter nicht. Deshalb setzen wir es bei Aktionen ein, die sich schwer rückgängig machen lassen (Löschen, Abmelden), oder bei Hinweisen, die vor dem Weitermachen gelesen werden müssen. Die Funktion showDialog, die den Dialog anzeigt, liefert ein Future zurück: Wir können mit await auf die Wahl des Nutzers warten und danach weitermachen.

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

Ein AlertDialog erscheint nie von selbst. Wir übergeben ihn an showDialog, das ihn als neue Route öffnet:

showDialog<void>(
  context: context,
  builder: (dialogContext) {
    return AlertDialog(
      title: const Text('Keine Verbindung'),
      content: const Text('Bitte Internetverbindung prüfen und erneut versuchen.'),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(dialogContext).pop(),
          child: const Text('OK'),
        ),
      ],
    );
  },
);

Den Parameter des Builders habe ich bewusst dialogContext genannt. Der Dialog ist eine eigene Route, und es ist die sicherste Gewohnheit, ihn mit seinem eigenen Context zu schließen statt mit dem der Seite. Warum, zeigt der Abschnitt über häufige Fehler.

Bestätigungsdialog und Warten auf das Ergebnis

Navigator.pop nimmt als zweites Argument einen Wert entgegen, und dieser Wert wird zum Ergebnis des Future, das showDialog zurückgibt:

showDialog liefert ein Future, Navigator.pop schließt mit einem zweiten Argument und der Wert kommt per await zurück

Future<void> _showConfirmDialog() async {
  final result = await showDialog<bool>(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: const Text('Löschen bestätigen'),
      content: const Text('Möchten Sie diesen Eintrag löschen?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext, false),
          child: const Text('NEIN'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(dialogContext, true),
          child: const Text('JA'),
        ),
      ],
    ),
  );

  if (result == true) {
    // Löschvorgang
  }
}

Der Rückgabetyp ist bool?. Tippt der Nutzer neben den Dialog oder drückt die Zurück-Taste, ist das Ergebnis null. Darum schreiben wir if (result == true) statt if (result): null zählt automatisch als „abgebrochen“. Wer bei await noch unsicher ist, liest am besten zuerst den Beitrag zu try-catch und async/await.

Eingaben erfassen

content akzeptiert jedes Widget. Für einen einzelnen Wert genügt ein TextField:

// In der State-Klasse: final controller = TextEditingController();

final name = await showDialog<String>(
  context: context,
  builder: (dialogContext) => AlertDialog(
    title: const Text('Name eingeben'),
    content: TextField(
      controller: controller,
      autofocus: true,
      decoration: const InputDecoration(hintText: 'Ihr Name...'),
    ),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(dialogContext),
        child: const Text('ABBRECHEN'),
      ),
      TextButton(
        onPressed: () => Navigator.pop(dialogContext, controller.text),
        child: const Text('SPEICHERN'),
      ),
    ],
  ),
);

Der Controller gehört in die State-Klasse und wird in dispose() freigegeben. controller.dispose() direkt nach dem Schließen des Dialogs aufzurufen wirkt ordentlich, führt aber zu einem Fehler, weil das TextField den Controller während der Schließanimation noch verwendet.

Sobald mehrere Felder und Validierung nötig sind, gehört ein Form-Widget in den Dialog – oder besser gleich eine eigene Seite.

Wichtige Eigenschaften

Eigenschaft Beschreibung
icon Symbol oberhalb des Titels
title Titel des Dialogs
content Inhalt des Dialogs
actions Buttons am unteren Rand
scrollable Macht Titel und Inhalt bei Überlänge scrollbar
shape Form des Dialogs
backgroundColor Hintergrundfarbe

barrierDismissible ist übrigens ein Parameter von showDialog, nicht von AlertDialog.

barrierDismissible

So verhindern Sie das Schließen beim Tippen außerhalb des Dialogs:

showDialog<void>(
  context: context,
  barrierDismissible: false, // Tippen außerhalb schließt nicht
  builder: (dialogContext) => PopScope(
    canPop: false, // Auch die Android-Zurück-Taste schließt nicht
    child: AlertDialog(
      title: const Text('Pflichtauswahl'),
      content: const Text('Zum Fortfahren müssen Sie eine Option wählen.'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: const Text('VERSTANDEN'),
        ),
      ],
    ),
  ),
);

barrierDismissible: false deaktiviert nur das Tippen außerhalb. Die Zurück-Taste unter Android schließt den Dialog weiterhin. Soll auch das verhindert werden, wird der Dialog in PopScope(canPop: false) eingepackt. Das sollte wirklich zwingenden Schritten vorbehalten bleiben, denn Nutzer einzusperren ist selten gute UX.

Plattformgerechte Dialoge: showAdaptiveDialog

Soll derselbe Code unter Android einen Material-Dialog und unter iOS und macOS einen Dialog im Cupertino-Stil öffnen, verwenden wir showAdaptiveDialog zusammen mit AlertDialog.adaptive:

showAdaptiveDialog<bool>(
  context: context,
  builder: (dialogContext) => AlertDialog.adaptive(
    title: const Text('Abmelden?'),
    content: const Text('Sie müssen sich danach erneut anmelden.'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(dialogContext, false),
        child: const Text('Abbrechen'),
      ),
      TextButton(
        onPressed: () => Navigator.pop(dialogContext, true),
        child: const Text('Abmelden'),
      ),
    ],
  ),
);

Der Rahmen des Dialogs passt sich der Plattform an, aber was in actions steht, wird unverändert gezeichnet. Damit auch die Buttons unter iOS nativ aussehen, schreibt man eine kleine Hilfsfunktion, die auf Apple-Plattformen CupertinoDialogAction statt TextButton zurückgibt.

Wann verwenden – und wann nicht?

Ein AlertDialog hält den Nutzer an. Diese Macht sollte man nur für Momente einsetzen, die es wert sind:

  • Verwenden für die Bestätigung unumkehrbarer Aktionen, Warnungen bei ungespeicherten Änderungen, die Begründung einer Berechtigung oder eine schnelle Einzeleingabe.
  • Nicht verwenden für reine Informationen wie „Gespeichert“. Sie dürfen den Ablauf nicht unterbrechen; das richtige Werkzeug ist die SnackBar.
  • Nicht verwenden für lange Optionslisten oder Formulare mit vielen Feldern. Für Optionslisten eignen sich showModalBottomSheet oder SimpleDialog besser, ein Formular ist auf einer eigenen Seite angenehmer. Wie ein Bottom Sheet Werte zurückgibt und seine Höhe steuert, zeigt der BottomSheet-Beitrag.

Eine Faustregel: Hat der Dialogtext mehr als zwei Sätze oder gibt es mehr als zwei Buttons, ist es vermutlich die falsche Komponente.

Häufige Fehler

1. Context nach einem await ungeprüft verwenden

Symptom: Der Analyzer meldet use_build_context_synchronously („Don't use 'BuildContext's across async gaps“). Zur Laufzeit arbeitet man, falls der Nutzer die Seite bei offenem Dialog verlassen hat, mit einem Context, der nicht mehr im Baum hängt, und die App wirft einen Fehler.

Lösung: Nach jedem await prüfen, bevor der Context angefasst wird.

final ok = await showDialog<bool>(context: context, builder: _buildDialog);
if (!context.mounted) return;
if (ok == true) Navigator.of(context).pop();

Innerhalb einer State-Klasse erfüllt if (!mounted) return; denselben Zweck.

2. setState aktualisiert den Dialog nicht

Symptom: Im Dialog steckt eine Checkbox, man tippt darauf, der Wert ändert sich, die Anzeige aber nicht. Schließt und öffnet man den Dialog erneut, ist der Haken plötzlich gesetzt.

Ursache: Der Dialog ist eine eigene Route. Das setState der Seite baut die Seite neu, nicht den Dialog.

Lösung: Den Dialoginhalt in einen StatefulBuilder packen (oder in ein eigenes StatefulWidget auslagern).

bool dontAskAgain = false;

showDialog<void>(
  context: context,
  builder: (dialogContext) => StatefulBuilder(
    builder: (context, setDialogState) => AlertDialog(
      title: const Text('Beenden'),
      content: CheckboxListTile(
        value: dontAskAgain,
        title: const Text('Nicht mehr fragen'),
        onChanged: (v) => setDialogState(() => dontAskAgain = v ?? false),
      ),
    ),
  ),
);

3. Überlaufender Inhalt

Symptom: Gelb-schwarze Streifen und „A RenderFlex overflowed by … pixels on the bottom“, sobald die Tastatur aufgeht oder der Bildschirm klein ist.

Lösung: Steht in content eine Column, bekommt sie mainAxisSize: MainAxisSize.min, und der AlertDialog erhält scrollable: true. Warum eine Column im Dialog mehr Platz beansprucht als nötig, erklärt das Dialog-Rezept im Beitrag zu mainAxisSize.

4. Dialog mit dem falschen Context schließen

Symptom: In Apps mit verschachtelten Navigator-Widgets (etwa Tabs mit eigenem Navigator) bleibt der Dialog nach dem Tippen auf den Button stehen, und die Seite darunter wird geschlossen.

Ursache: showDialog nutzt standardmäßig den Root-Navigator, während Navigator.pop(context) mit dem Context der Seite den nächstgelegenen, inneren Navigator findet.

Lösung: Den Dialog mit dem dialogContext schließen, den der builder liefert. Genau deshalb verwenden alle Beispiele in diesem Beitrag diesen Namen.

Mini-Szenario: Vor dem Löschen fragen, danach benachrichtigen

Stellen wir uns eine Notiz-App vor. Will der Nutzer eine Notiz löschen, fragen wir zuerst nach, und nach dem Löschen zeigen wir eine kurze Meldung. Das Modell Note und das Objekt repository werden als vorhanden vorausgesetzt:

Future<void> _confirmAndDelete(Note note) async {
  final confirmed = await showDialog<bool>(
    context: context,
    builder: (dialogContext) => AlertDialog(
      icon: const Icon(Icons.delete_outline),
      title: const Text('Notiz löschen?'),
      content: Text('„${note.title}“ wird endgültig gelöscht.'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext, false),
          child: const Text('Abbrechen'),
        ),
        FilledButton(
          onPressed: () => Navigator.pop(dialogContext, true),
          child: const Text('Löschen'),
        ),
      ],
    ),
  );

  if (confirmed != true) return; // Abgebrochen oder daneben getippt

  await widget.repository.delete(note.id);

  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('„${note.title}“ gelöscht')),
  );
}

Drei Entscheidungen sind hier wichtig. Erstens stellt der Titel die Frage und die Buttons geben die Antwort: Mit „Löschen/Abbrechen“ statt „Ja/Nein“ weiß der Nutzer auch ohne Lesen des Textes, worauf er tippt. Zweitens ist die gefährliche Aktion mit einem FilledButton hervorgehoben, das Abbrechen bleibt ein ruhiger TextButton. Drittens wird der Context nach zwei await-Aufrufen nie ohne mounted-Prüfung angefasst. Das Ergebnis selbst meldet eine SnackBar und kein weiterer Dialog, denn es gibt keinen Grund mehr, den Nutzer anzuhalten.

Häufig gestellte Fragen

Wie verhindere ich, dass ein AlertDialog beim Tippen außerhalb schließt?

Übergeben Sie barrierDismissible: false an showDialog. Soll auch die Android-Zurück-Taste den Dialog nicht schließen, packen Sie ihn zusätzlich in PopScope(canPop: false).

Warum liefert showDialog null zurück?

Schließt der Nutzer den Dialog, ohne einen Ihrer Buttons zu drücken – durch Tippen außerhalb oder mit der Zurück-Taste –, wird die Route ohne Wert entfernt und das Ergebnis ist null. Vergleichen Sie das Ergebnis deshalb mit result == true.

Was ist der Unterschied zwischen AlertDialog und SimpleDialog?

AlertDialog ist für eine Meldung mit Aktionsbuttons darunter gedacht. SimpleDialog hat keine Buttonzeile und zeigt eine kurze Auswahlliste aus SimpleDialogOption-Einträgen.

Wie sieht der Dialog unter iOS nativ aus?

Mit showAdaptiveDialog und AlertDialog.adaptive erhalten iOS und macOS einen Dialog im Cupertino-Stil. Für nativ wirkende Buttons verwendet man auf diesen Plattformen CupertinoDialogAction.

Kommentare