Start Debugging

BuildContext nach einem await in Flutter sicher verwenden

Lesen Sie alles Benötigte vor dem await aus dem context aus und schützen Sie die Fortsetzung mit if (context.mounted) return. Hier das vollständige Muster, die Linter-Regel, die es erzwingt, und die Randfälle, die sie übersieht.

Die Regel ist kurz: Ein BuildContext ist nur gültig, solange sein Widget gemountet ist, und ein await kann das Widget unmounten, bevor Ihr Code fortgesetzt wird. Lesen Sie deshalb alles, was Sie benötigen, vor dem ersten await aus dem context aus (einen NavigatorState, einen ScaffoldMessengerState, einen Themenwert), führen Sie die asynchrone Arbeit aus und schützen Sie dann die Fortsetzung mit if (!context.mounted) return;, bevor Sie den context erneut anfassen. Diese eine Gewohnheit verhindert die gesamte Familie der Abstürze nach dem Muster “context verwendet, nachdem er den Baum verlassen hatte”. Diese Anleitung verwendet Flutter 3.44 (stable, Mai 2026) und Dart 3.x.

Ein BuildContext ist kein Datenbeutel, den Sie wegspeichern und wiederverwenden können. Er ist ein lebendiges Handle auf ein Element im Widget-Baum. In dem Moment, in dem der Benutzer wegnavigiert, das Elternteil Sie aus der Existenz neu aufbaut oder die Route entfernt wird, wird dieses Element deaktiviert und anschließend verworfen. Einen Vorfahren von einem toten Element aus zu lesen (Navigator.of, Theme.of, Provider.of) ist undefiniert: Im Debug erhalten Sie eine Assertion, im Release einen veralteten Wert oder viel später eine Null-Dereferenzierung. Der asynchrone Fall schmerzt am meisten, weil die Lücke zwischen “context war gültig” und “context wird verwendet” im Quellcode unsichtbar ist: Sie verbirgt sich im await.

Warum das await der gefährliche Teil ist

Flutter ruft build synchron auf und erwartet, dass es fertig wird, bevor irgendetwas anderes den Baum anfasst. Solange Ihr Code synchron aus einem Event-Handler läuft, bleibt der context die ganze Zeit gültig. In dem Augenblick, in dem Sie await aufrufen, geben Sie die Kontrolle an die Event-Schleife zurück. Andere Frames laufen. Der Benutzer kann die Zurück-Schaltfläche antippen, ein übergeordneter StreamBuilder kann sich neu aufbauen, ein Timeout kann ein Entfernen der Route auslösen. Wenn Ihre Fortsetzung fortgesetzt wird, befinden Sie sich in einem späteren Frame, und das Widget, dem context gehörte, kann verschwunden sein.

// Flutter 3.44, Dart 3.x -- the gap is invisible but real
Future<void> _onSave() async {
  await api.save(form);            // <-- control leaves here, frames run
  Navigator.of(context).pop();     // <-- may execute on a dead context
}

Nichts in _onSave sieht falsch aus. Der Fehler ist strukturell: context wurde an der Aufrufstelle implizit erfasst und über einen Suspendierungspunkt hinweg wiederverwendet. Genau diese Situation beschreibt der Absturz beim Nachschlagen des Vorfahren eines deaktivierten Widgets von der Seite der Fehlermeldung her. Hier betrachten wir ihn von der Seite der Vermeidung.

Das sichere Muster, Schritt für Schritt

Befolgen Sie diese vier Schritte immer dann, wenn eine asynchrone Methode nach ihrer Suspendierung einen context benötigt. Die ersten beiden sind die tragenden; der Rest ist, wie Sie sie ehrlich halten.

  1. Lesen Sie alles aus dem context, bevor das erste await kommt. Lösen Sie Navigator.of(context), ScaffoldMessenger.of(context), Theme.of(context) und jeden Provider.of-/context.read-Aufruf in lokale Variablen auf, solange das Widget noch gemountet ist. Diese liefern langlebige State-Objekte zurück, die selbst dann gültig bleiben, wenn das ursprüngliche Element stirbt.
  2. Führen Sie die asynchrone Arbeit aus. Jetzt darf das await so lange dauern, wie es will. Sie halten den context nicht über es hinweg; Sie halten die aufgelösten State-Objekte, die das Element überleben.
  3. Schützen Sie die Fortsetzung mit einer mounted-Prüfung. Schreiben Sie unmittelbar nach dem await if (!context.mounted) return; (oder if (!mounted) return; innerhalb eines State). Wenn das Widget den Baum während des await verlassen hat, halten Sie hier an und fassen niemals einen toten context an.
  4. Verwenden Sie nach der Lücke nur die erfassten Objekte. Rufen Sie navigator.pop() und messenger.showSnackBar(...) auf den lokalen Variablen auf, die Sie in Schritt 1 erfasst haben, nicht erneut auf Navigator.of(context).

Auf das fehlerhafte Beispiel angewendet:

// Flutter 3.44, Dart 3.x -- safe
Future<void> _onSave() async {
  final navigator = Navigator.of(context);          // 1. capture
  final messenger = ScaffoldMessenger.of(context);

  await api.save(form);                              // 2. async work

  if (!context.mounted) return;                      // 3. guard

  messenger.showSnackBar(                            // 4. use captures
    const SnackBar(content: Text('Saved')),
  );
  navigator.pop();
}

Zwei unabhängige Dinge machen das korrekt. navigator und messenger vor dem await zu erfassen bedeutet, dass Sie .of(context) nie auf einem deaktivierten Element aufrufen. Die context.mounted-Prüfung überspringt dann die UI-Arbeit vollständig, wenn der Benutzer bereits gegangen ist, was fast immer das gewünschte Verhalten ist: Es ergibt keinen Sinn, eine Snackbar auf einem Bildschirm anzuzeigen, den niemand betrachtet.

mounted auf State versus mounted auf BuildContext

Es gibt zwei mounted-Getter, und sie sind nicht austauschbar, was den Ort betrifft, an dem Sie auf sie zurückgreifen, auch wenn sie dieselbe Frage beantworten.

State.mounted gibt es schon immer. Schreiben Sie innerhalb der State-Klasse eines StatefulWidget if (!mounted) return;. Es ist true zwischen initState und dispose und, entscheidend, bereits false während deactivate, sodass es den Fall “das Widget geht gerade” korrekt erfasst, nicht nur den Fall “das Widget ist vollständig tot”.

BuildContext.mounted kam in Flutter 3.7 (Dart 2.19) für den Fall hinzu, dass Sie nur einen context haben, kein State: Hilfsfunktionen, Callbacks in einem StatelessWidget, Extension-Methoden. Es liefert zurück, ob das zugrunde liegende Element noch gemountet ist.

// Flutter 3.44, Dart 3.x
// Inside a State subclass:
if (!mounted) return;          // State.mounted

// In a helper that only has a context:
if (!context.mounted) return;  // BuildContext.mounted

Bevorzugen Sie State.mounted, wenn Sie sich innerhalb einer State-Klasse befinden, weil es den Lebenszyklus des Widgets liest, das Ihnen tatsächlich gehört. Verwenden Sie context.mounted, wenn ein context alles ist, was Sie haben. Beide müssen nach dem await geprüft werden, niemals davor: Die Lücke ist das await, also sagt eine Prüfung, die davor läuft, nichts über den Zustand danach aus.

Warum Erfassen allein nicht genügt und Schützen allein nicht genügt

Häufig macht man eine der beiden Hälften und nimmt an, abgesichert zu sein. Man ist es nicht.

Wenn Sie nur erfassen, aber den Schutz auslassen, vermeiden Sie den Absturz durch den deaktivierten context, können aber dennoch UI-Seiteneffekte gegen einen Bildschirm ausführen, den der Benutzer bereits verlassen hat: eine Snackbar, die auf der falschen Route aufblitzt, ein pop(), das eine Route entfernt, die nicht mehr Ihre ist. Erfassen macht den Aufruf zulässig; der Schutz macht ihn korrekt.

Wenn Sie nur schützen, aber das Erfassen auslassen, haben Sie einen subtilen Reihenfolgefehler. Betrachten Sie:

// Flutter 3.44, Dart 3.x -- still wrong despite the guard
Future<void> _onSave() async {
  await api.save(form);
  if (!context.mounted) return;
  Navigator.of(context).pop();   // re-reads context AFTER the gap
}

Das funktioniert meist, weil die context.mounted-Prüfung im selben synchronen Tick wie der Navigator.of-Aufruf durchlief. Aber es ist brüchig: Wenn Sie ein zweites await zwischen die Prüfung und das Nachschlagen einfügen, öffnet sich das Fenster wieder. Das Muster, zuerst zu erfassen, entfernt das Nachschlagen vollständig vom Pfad nach dem await, sodass nichts übrig bleibt, das veralten könnte. Behandeln Sie “vorher erfassen, danach schützen, die Erfassungen verwenden” als eine unteilbare Bewegung.

Die Linter-Regel, die es erzwingt: use_build_context_synchronously

Dart liefert eine Linter-Regel namens use_build_context_synchronously, die einen BuildContext markiert, der nach einer asynchronen Lücke ohne einen mounted-Schutz zwischen await und Verwendung benutzt wird. Sie ist im Paket flutter_lints standardmäßig aktiviert, das neue Flutter-Projekte über analysis_options.yaml einbinden:

# analysis_options.yaml -- on by default in flutter_lints
include: package:flutter_lints/flutter.yaml

Wenn Ihr Projekt älter als der Standard ist oder Sie das Include entfernt haben, fügen Sie die Regel explizit hinzu:

# analysis_options.yaml
linter:
  rules:
    use_build_context_synchronously: true

Die Regel versteht den Schutz. if (!context.mounted) return; (oder if (context.mounted) { ... }) nach dem await zu schreiben, beseitigt die Warnung, weil der Analyzer beweisen kann, dass der context auf dem Pfad, der ihn verwendet, lebendig ist. Deshalb ist die kanonische Form if (context.mounted) und nicht irgendein von Hand geschriebenes Äquivalent: Der Linter führt einen Mustervergleich mit den als sicher bekannten Formen durch. Frühere Versionen des Analyzers erzeugten sogar einen False Positive, wenn BuildContext.mounted außerhalb der wörtlichen Form if (context.mounted) {} verwendet wurde, festgehalten in der Issue-Liste des Dart-SDK; die aktuellen Versionen behandeln die gängigen Formen, aber es ist ein weiterer Grund, beim idiomatischen Schutz zu bleiben.

Was der Linter nicht erfasst, ist genauso wichtig. Es handelt sich um eine syntaktische Prüfung, sie kann also nicht über Funktionsgrenzen hinwegsehen. Wenn Sie einen BuildContext an eine Hilfsfunktion übergeben und innerhalb dieser Funktion await aufrufen, kann der Analyzer die Lücke oft nicht mit der späteren Verwendung verbinden. Er rettet Sie auch nicht vor einer Logik, die einen context in einem Feld erfasst und ihn viel später wiederverwendet. Der Linter ist eine starke erste Verteidigungslinie, kein Beweis.

Einen context an eine Hilfsfunktion übergeben

Ein häufiges Entkommen vor dem Linter ist, das await in eine Hilfsfunktion zu verschieben, die BuildContext als Parameter nimmt. Das Muster ist in Ordnung, aber die Hilfsfunktion übernimmt nun die Verantwortung für den Schutz und sollte mounted selbst erneut prüfen, statt sich auf den Aufrufer zu verlassen.

// Flutter 3.44, Dart 3.x -- the helper guards its own context use
Future<void> confirmAndDelete(BuildContext context, Item item) async {
  final messenger = ScaffoldMessenger.of(context);

  final ok = await showDialog<bool>(
    context: context,
    builder: (_) => const ConfirmDialog(),
  );

  if (ok != true) return;
  if (!context.mounted) return;   // guard inside the helper

  await repository.delete(item);
  if (!context.mounted) return;   // second await, second guard

  messenger.showSnackBar(const SnackBar(content: Text('Deleted')));
}

Zwei awaits bedeuten zwei Schutzprüfungen. Jeder Suspendierungspunkt öffnet das Fenster erneut, also gehört eine mounted-Prüfung nach jeden, der einer context-Verwendung vorausgeht, nicht nur nach dem ersten. messenger vorab zu erfassen bedeutet, dass die letzte Zeile den context nie erneut liest.

Schleifen, Wiederholungsversuche und mehrere awaits

Überall dort, wo eine context-Verwendung nach mehr als einer möglichen Suspendierung steht, prüfen Sie jeden Pfad. Eine Wiederholungsschleife ist der Lehrbuchfall:

// Flutter 3.44, Dart 3.x
Future<void> _uploadWithRetry() async {
  final messenger = ScaffoldMessenger.of(context);

  for (var attempt = 1; attempt <= 3; attempt++) {
    try {
      await api.upload(file);     // suspension point inside the loop
      break;
    } catch (_) {
      if (attempt == 3) rethrow;
      await Future<void>.delayed(const Duration(seconds: 1)); // another one
    }
  }

  if (!context.mounted) return;   // single guard after the loop is enough
  messenger.showSnackBar(const SnackBar(content: Text('Uploaded')));
}

Hier brauchen Sie keinen Schutz innerhalb der Schleife, weil nichts innerhalb der Schleife den context anfasst; die einzige context-Verwendung ist nach ihr, also deckt ein einziger Schutz alle Ausgangspfade ab. Das Prinzip verallgemeinert sich: Platzieren Sie den Schutz unmittelbar vor jeder context-Verwendung, nach dem letzten await, das ihr vorausgehen kann. Auf einen strukturierten Ansatz wie die elegante Behandlung von Fehler- und Ladezuständen zurückzugreifen, hält diese Abläufe lesbar, weil die Wiederholungs- und Fehlerzustände zu Daten werden, die Ihr Widget rendert, statt zu imperativen UI-Aufrufen, die nach den awaits verstreut sind.

StatelessWidget hat kein mounted, verwenden Sie also den context

Ein StatelessWidget hat keinen State, also gibt es kein mounted-Feld. Verwenden Sie context.mounted, wofür es genau existiert:

// Flutter 3.44, Dart 3.x -- StatelessWidget callback
ElevatedButton(
  onPressed: () async {
    final navigator = Navigator.of(context);
    await Future<void>.delayed(const Duration(seconds: 1));
    if (!context.mounted) return;
    navigator.pop();
  },
  child: const Text('Close'),
);

Wenn Sie merken, dass Sie in den Callbacks eines zustandslosen Widgets mehrere Schutzprüfungen benötigen, ist das oft ein Signal, dass das Widget stateful sein sollte oder dass die asynchrone Arbeit eher zu einem Controller oder Notifier gehört als inline in den Button-Handler.

Fallstricke und Verwechslungen

Navigator.pop, dann eine context-Verwendung. Ein klassischer Zweizeiler: Navigator.pop(context), gefolgt von einem weiteren .of(context)-Aufruf. Das pop beginnt, das Element der Route zu deaktivieren, sodass das zweite Nachschlagen fehlschlagen kann, obwohl kein await in Sicht ist. Erfassen Sie den navigator (und alles andere) vor dem Entfernen.

initState kann keine inherited-Nachschlagungen durchführen. Theme.of, MediaQuery.of und jedes dependOnInheritedWidgetOfExactType sind in initState unzulässig, weil das Element noch nicht mit seinen geerbten Abhängigkeiten verdrahtet ist. Verschieben Sie diese Lesevorgänge nach didChangeDependencies, wo der context vollständig gültig ist. Das ist eine andere Assertion als die asynchrone, aber sie entspringt derselben Frage “ist der context gerade jetzt gültig?”.

Release-Builds verbergen den Absturz. Die Assertion des deaktivierten context feuert nur im Debug. In Profile und Release liefert das Nachschlagen null zurück, und Sie erhalten irgendwo weiter unten ein Null check operator used on a null value. Wenn ein Absturz nur im Release und nur nach dem Navigieren auftritt, verdächtigen Sie eine ungeschützte context-Verwendung nach dem await. Der Schutz vor während build aufgerufenem setState hat denselben Charakter einer nur im Debug aktiven Assertion.

Das Riverpod-Pendant. Wenn Sie einen WidgetRef statt eines BuildContext halten, ist der entsprechende Absturz Cannot use “ref” after the widget was disposed. Dieselbe Grundursache, dieselbe Lösung: vor dem await lesen, danach schützen. Asynchrone Arbeit als Lade- und Fehlerzustände mit AsyncValue zu modellieren, umgeht die meisten manuellen Schutzprüfungen, weil das Framework den Lebenszyklus des Widgets für Sie verfolgt und Sie aufhören, den context von Hand anzufassen.

Timer und Stream-Listener. Ein context, der in einem Timer, einem Stream.listen oder einem Animations-Status-Listener verwendet wird, kann feuern, nachdem das Widget weg ist. Schützen Sie mit mounted, und brechen Sie außerdem die Quelle in dispose ab, damit der Callback gar nicht mehr feuert, dieselbe Disziplin, die Sie anwenden, wenn Sie Controller verwerfen, um Lecks zu vermeiden.

Die eine Gewohnheit, die diese ganze Fehlerklasse abschafft

Behandeln Sie einen BuildContext nur vom Beginn eines synchronen Laufs bis zum nächsten await als gültig. Bevor Sie suspendieren, lesen Sie die State-Objekte aus, die Sie brauchen werden. Nachdem Sie fortgesetzt haben, prüfen Sie mounted, bevor Sie irgendetwas anfassen, das an den Baum gebunden ist. Tun Sie das mechanisch, und der Absturz des deaktivierten Vorfahren, der Absturz des verworfenen Controllers und die Null-Dereferenzierung nach dem await hören auf aufzutreten, denn es waren nie drei Fehler. Es war eine Regel, auf drei Arten gebrochen.

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück