Flutter Widget Lifecycle: Von initState bis dispose
# Flutter Widget Lifecycle: Von initState bis dispose
Den Lebenszyklus von StatefulWidgets zu beherrschen gehoert zu den entscheidenden Faehigkeiten in der Flutter-Entwicklung. Wer ihn richtig handhabt, baut schnelle, stabile und leckfreie Screens. Wer ihn falsch handhabt, jagt um zwei Uhr nachts raetselhafte Abstuerze. In diesem Artikel gehe ich jeden Lifecycle-Method im Detail durch, mit echtem Code und Ratschlaegen, die ich gerne frueher bekommen haette.
Der Lebenszyklus im Ueberblick
Bevor wir in die einzelnen Methoden eintauchen, hier der vollstaendige Ablauf eines StatefulWidgets von der Erstellung bis zur Zerstoerung:
createState()
|
v
initState()
|
v
didChangeDependencies()
|
v
build() <------------------+
| |
v |
[Widget lebt auf dem Screen]|
| |
v |
didUpdateWidget() ---------+
|
v (oder setState() loest Rebuild ueber build() aus)
|
deactivate()
|
v
dispose()Jedes StatefulWidget durchlaeuft diesen Pfad. Die zentrale Erkenntnis: `build` wird viele Male aufgerufen, waehrend `initState` und `dispose` genau einmal ausgefuehrt werden.
Die Lifecycle-Methoden im Detail
createState()
Der Einstiegspunkt. Flutter ruft diese Methode auf dem StatefulWidget auf, um das State-Objekt zu erzeugen. Sie wird einmal ausgefuehrt und sollte keine Logik enthalten ausser der Rueckgabe der State-Instanz.
class ProfilScreen extends StatefulWidget {
final String nutzerId;
const ProfilScreen({super.key, required this.nutzerId});
@override
State<ProfilScreen> createState() => _ProfilScreenState();
}initState()
Wird einmal aufgerufen, unmittelbar nach der Erstellung des State-Objekts. Hier werden Controller eingerichtet, Animationen gestartet, einmalige Datenabrufe angestossen und Stream-Abonnements angelegt.
@override
void initState() {
super.initState();
_tabController = TabController(length: class="code-number">3, vsync: this);
_scrollController = ScrollController();
_animationController = AnimationController(
duration: const Duration(milliseconds: class="code-number">300),
vsync: this,
);
_nutzerdatenLaden();
}Regeln:
didChangeDependencies()
Wird direkt nach `initState` aufgerufen, und danach jedes Mal, wenn ein InheritedWidget, von dem dieser State abhaengt, sich aendert. Dies ist der richtige Ort, um `Theme.of(context)`, `MediaQuery.of(context)` oder jede Provider-/InheritedWidget-Abfrage durchzufuehren, die zur Initialisierung gebraucht wird.
@override
void didChangeDependencies() {
super.didChangeDependencies();
class=class="code-string">"code-comment">// context ist hier sicher verwendbar
final theme = Theme.of(context);
_hintergrundFarbe = theme.colorScheme.surface;
final locale = Localizations.localeOf(context);
if (_aktuelleLocale != locale) {
_aktuelleLocale = locale;
_lokalisiertenInhaltNeuLaden();
}
}Ein Fehler, den ich in Code-Reviews haeufig sehe: Entwickler platzieren `MediaQuery.of(context)` in `initState` und wundern sich ueber die Fehlermeldungen. Der Widget-Baum ist an diesem Punkt noch nicht fertig aufgebaut. Verschieben Sie solche Abfragen hierher.
build()
Die am haeufigsten aufgerufene Methode im gesamten Lebenszyklus. Flutter ruft `build` auf, wenn sich der State aendert, wenn das Eltern-Widget neu baut, oder wenn sich eine InheritedWidget-Abhaengigkeit aendert. Sie muss rein und schnell sein: keine Seiteneffekte, keine aufwaendigen Berechnungen, keine Netzwerkanfragen.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(_nutzerName)),
body: _laed
? const Center(child: CircularProgressIndicator())
: ListView.builder(
controller: _scrollController,
itemCount: _elemente.length,
itemBuilder: (context, index) => ElementKarte(element: _elemente[index]),
),
);
}Regeln:
didUpdateWidget()
Wird aufgerufen, wenn das Eltern-Widget neu baut und neue Konfiguration an dieses Widget uebergibt. Das Framework stellt das alte Widget bereit, sodass Sie vergleichen und entscheiden koennen, ob eine Reaktion noetig ist.
@override
void didUpdateWidget(covariant ProfilScreen altesWidget) {
super.didUpdateWidget(altesWidget);
if (altesWidget.nutzerId != widget.nutzerId) {
class=class="code-string">"code-comment">// Der Nutzer hat gewechselt — Daten neu laden
_nutzerdatenLaden();
}
}Hier ist der richtige Ort, um Animationen neu zu starten, Daten abzurufen oder Controller zu aktualisieren, wenn das Widget neue Parameter vom Eltern-Widget erhaelt.
deactivate()
Wird aufgerufen, wenn der State aus dem Baum entfernt wird. Das kann voruebergehend sein, wenn das Widget ueber einen GlobalKey an eine andere Stelle im Baum verschoben wird. In den meisten Faellen muessen Sie diese Methode nicht ueberschreiben, aber sie existiert fuer fortgeschrittene Anwendungsfaelle.
@override
void deactivate() {
class=class="code-string">"code-comment">// Bereinigung von allem, was an die Position im Baum gebunden ist
super.deactivate();
}dispose()
Wird einmal aufgerufen, wenn das State-Objekt dauerhaft entfernt wird. Dies ist die letzte Gelegenheit, Ressourcen freizugeben. Das Vergessen der Bereinigung an dieser Stelle ist die Hauptursache fuer Memory Leaks in Flutter-Apps.
@override
void dispose() {
_tabController.dispose();
_scrollController.dispose();
_animationController.dispose();
_abonnement?.cancel();
_focusNode.dispose();
super.dispose();
}Regeln:
Vollstaendiges Beispiel: Alle Lifecycle-Methoden zusammen
class LiveDatenScreen extends StatefulWidget {
final String kanalId;
const LiveDatenScreen({super.key, required this.kanalId});
@override
State<LiveDatenScreen> createState() => _LiveDatenScreenState();
}
class _LiveDatenScreenState extends State<LiveDatenScreen>
with SingleTickerProviderStateMixin {
late final AnimationController _animController;
late final ScrollController _scrollController;
StreamSubscription<DatenEreignis>? _datenAbonnement;
List<DatenEreignis> _ereignisse = [];
bool _laed = true;
bool _eingehaengt = true;
class=class="code-string">"code-comment">// class="code-number">1. initState — einmalige Einrichtung
@override
void initState() {
super.initState();
_animController = AnimationController(
duration: const Duration(milliseconds: class="code-number">400),
vsync: this,
);
_scrollController = ScrollController();
_kanalAbonnieren(widget.kanalId);
}
class=class="code-string">"code-comment">// class="code-number">2. didChangeDependencies — kontextabhaengige Initialisierung
@override
void didChangeDependencies() {
super.didChangeDependencies();
final helligkeit = Theme.of(context).brightness;
_animController.duration = helligkeit == Brightness.dark
? const Duration(milliseconds: class="code-number">600)
: const Duration(milliseconds: class="code-number">400);
}
class=class="code-string">"code-comment">// class="code-number">3. build — reine UI-Beschreibung
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(class="code-string">'Kanal: ${widget.kanalId}')),
body: _laed
? const Center(child: CircularProgressIndicator())
: ListView.builder(
controller: _scrollController,
itemCount: _ereignisse.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(_ereignisse[index].nachricht),
subtitle: Text(_ereignisse[index].zeitstempel.toString()),
);
},
),
);
}
class=class="code-string">"code-comment">// class="code-number">4. didUpdateWidget — auf Aenderungen des Eltern-Widgets reagieren
@override
void didUpdateWidget(covariant LiveDatenScreen altesWidget) {
super.didUpdateWidget(altesWidget);
if (altesWidget.kanalId != widget.kanalId) {
_datenAbonnement?.cancel();
_ereignisse = [];
_kanalAbonnieren(widget.kanalId);
}
}
class=class="code-string">"code-comment">// class="code-number">5. deactivate — voruebergehende Entfernung aus dem Baum
@override
void deactivate() {
super.deactivate();
}
class=class="code-string">"code-comment">// class="code-number">6. dispose — dauerhafte Bereinigung
@override
void dispose() {
_eingehaengt = false;
_datenAbonnement?.cancel();
_animController.dispose();
_scrollController.dispose();
super.dispose();
}
class=class="code-string">"code-comment">// Hilfsmethode
void _kanalAbonnieren(String kanalId) {
_datenAbonnement = DatenService.abonnieren(kanalId).listen(
(ereignis) {
if (!_eingehaengt) return;
setState(() {
_ereignisse.add(ereignis);
_laed = false;
});
},
onError: (fehler) {
debugPrint(class="code-string">'Stream-Fehler: $fehler');
},
);
}
}Memory-Leak-Praevention
Memory Leaks in Flutter lassen sich fast immer auf eine Ursache zurueckfuehren: Irgendetwas haelt eine Referenz auf das State-Objekt, nachdem es disposed wurde. Hier sind die haeufigsten Verursacher und ihre Loesungen.
StreamSubscription-Leaks
class=class="code-string">"code-comment">// SCHLECHT: Abonnement wird nie gekuendigt
@override
void initState() {
super.initState();
FirebaseFirestore.instance
.collection(class="code-string">'nachrichten')
.snapshots()
.listen((snapshot) {
setState(() {
_nachrichten = snapshot.docs;
});
});
}
class=class="code-string">"code-comment">// GUT: Abonnement speichern und in dispose kuendigen
late final StreamSubscription _nachrichtenAbo;
@override
void initState() {
super.initState();
_nachrichtenAbo = FirebaseFirestore.instance
.collection(class="code-string">'nachrichten')
.snapshots()
.listen((snapshot) {
setState(() {
_nachrichten = snapshot.docs;
});
});
}
@override
void dispose() {
_nachrichtenAbo.cancel();
super.dispose();
}AnimationController-Leaks
Jeder `AnimationController` reserviert Ticker-Ressourcen. Wird er nicht disposed, feuert der Ticker weiter Frame fuer Frame, obwohl das Widget laengst verschwunden ist.
class=class="code-string">"code-comment">// SCHLECHT: Controller leckt
late final AnimationController _ctrl;
@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: Durations.medium1);
_ctrl.repeat();
}
class=class="code-string">"code-comment">// GUT: Controller disposen
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}TextEditingController- und FocusNode-Leaks
Diese wirken leichtgewichtig, registrieren aber intern Listener. Vergessen Sie nicht, sie zu disposen.
@override
void dispose() {
_emailController.dispose();
_passwortController.dispose();
_emailFocusNode.dispose();
_passwortFocusNode.dispose();
super.dispose();
}Das Async-Gap-Problem
Die haeufigste Ursache fuer "setState called after dispose" ist eine asynchrone Operation, die abschliesst, nachdem der Nutzer bereits weg navigiert hat.
class=class="code-string">"code-comment">// SCHLECHT: keine Absicherung
Future<void> _datenLaden() async {
final daten = await repository.elementeAbrufen();
setState(() {
_elemente = daten;
});
}
class=class="code-string">"code-comment">// GUT: mounted pruefen
Future<void> _datenLaden() async {
final daten = await repository.elementeAbrufen();
if (!mounted) return;
setState(() {
_elemente = daten;
});
}Die `mounted`-Eigenschaft ist in jedem State-Objekt eingebaut. Pruefen Sie sie nach jedem `await`.
StatefulWidget vs StatelessWidget vs ConsumerWidget: Wann welches?
Die Wahl des richtigen Widget-Typs ist eine Designentscheidung, die Lesbarkeit, Testbarkeit und Performance direkt beeinflusst.
StatelessWidget
Verwenden Sie es, wenn das Widget keinerlei veraenderlichen State hat. Es empfaengt alles ueber seinen Konstruktor und rendert es.
class NutzerAvatar extends StatelessWidget {
final String bildUrl;
final double radius;
const NutzerAvatar({super.key, required this.bildUrl, this.radius = class="code-number">24});
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: radius,
backgroundImage: NetworkImage(bildUrl),
);
}
}StatefulWidget
Verwenden Sie es, wenn Sie lokalen, widget-bezogenen veraenderlichen State benoetigen: AnimationController, TextEditingController, Scroll-Position-Tracking, Formularvalidierung oder jeder State, der nicht in einen globalen Store gehoert.
class=class="code-string">"code-comment">// Guter Einsatz von StatefulWidget: lokaler UI-State
class AusklappbareKarte extends StatefulWidget { ... }
class _AusklappbareKarteState extends State<AusklappbareKarte> {
bool _istAusgeklappt = false;
class=class="code-string">"code-comment">// Dieser State ist rein UI-lokal — kein Provider noetig
}ConsumerWidget (Riverpod)
Verwenden Sie es, wenn das Widget State lesen oder auf State reagieren muss, der von Riverpod-Providern verwaltet wird. ConsumerWidget liefert ein `ref`-Objekt zum Beobachten von Providern, ohne ein StatefulWidget zu benoetigen.
class NutzerProfilAnsicht extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final nutzer = ref.watch(nutzerProvider);
return nutzer.when(
data: (daten) => Text(daten.name),
loading: () => const CircularProgressIndicator(),
error: (f, st) => Text(class="code-string">'Fehler: $f'),
);
}
}Entscheidungsablauf
Ein Fehler, den ich in Code-Reviews haeufig sehe: Entwickler greifen jedes Mal zu StatefulWidget, wenn sie "etwas machen" muessen. Oft gehoert der verwaltete State eigentlich in einen Provider, und das Widget sollte ein ConsumerWidget sein. In anderen Faellen ist das Widget rein praesentational, und ein StatelessWidget waere einfacher und performanter.
Debugging von Lifecycle-Problemen
Wenn etwas schiefgeht, liefert Flutter meist hilfreiche Fehlermeldungen. Aber zu wissen, wo man suchen muss, kann Stunden sparen.
"setState() called after dispose()"
Das bedeutet, dass ein asynchroner Callback, ein Stream-Listener oder ein Animations-Callback versucht hat, den State eines nicht mehr existierenden Widgets zu aktualisieren. Die Loesung ist immer dieselbe: mit `mounted` absichern oder die Quelle kuendigen.
class=class="code-string">"code-comment">// Fuegen Sie dies Ihren async-Methoden hinzu
if (!mounted) return;Widget baut zu oft neu
Verwenden Sie `debugPrint` innerhalb von `build`, um zu sehen, wie oft es aufgerufen wird. Wenn ein Widget bei jedem Frame neu baut, pruefen Sie, ob ein Vorfahren-Widget `setState` zu breit aufruft.
@override
Widget build(BuildContext context) {
debugPrint(class="code-string">'${widget.runtimeType} build aufgerufen');
class=class="code-string">"code-comment">// ...
}Fuer eine tiefere Analyse verwenden Sie den Flutter DevTools Widget Inspector. Er hebt Rebuilds in Echtzeit hervor.
initState wird unerwartet erneut ausgefuehrt
Wenn `initState` mehr als einmal feuert, bedeutet das, dass Ihr Widget zerstoert und neu erstellt wird. Haeufige Ursachen:
Um den State ueber solche Ereignisse hinweg zu erhalten, ziehen Sie `AutomaticKeepAliveClientMixin` in Betracht oder verschieben Sie den State in einen Provider.
Den gesamten Lebenszyklus loggen
Waehrend der Entwicklung koennen Sie in jede Lifecycle-Methode Prints einfuegen, um genau zu verstehen, was passiert:
@override
void initState() {
super.initState();
debugPrint(class="code-string">'[Lifecycle] initState');
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint(class="code-string">'[Lifecycle] didChangeDependencies');
}
@override
void didUpdateWidget(covariant MeinWidget altesWidget) {
super.didUpdateWidget(altesWidget);
debugPrint(class="code-string">'[Lifecycle] didUpdateWidget');
}
@override
void deactivate() {
debugPrint(class="code-string">'[Lifecycle] deactivate');
super.deactivate();
}
@override
void dispose() {
debugPrint(class="code-string">'[Lifecycle] dispose');
super.dispose();
}Fazit
Lifecycle-Disziplin ist nicht glamouroes, aber sie macht den Unterschied zwischen einer App, die sich solide anfuehlt, und einer, die in Produktion gelegentlich mit raetselhaften Fehlern abstuerzt. Beherrschen Sie `initState` fuer die Einrichtung, `didChangeDependencies` fuer kontextabhaengige Initialisierung, `build` fuer reines Rendering, `didUpdateWidget` fuer Reaktionen auf Eltern-Aenderungen und `dispose` fuer die Bereinigung. Sichern Sie jede async-Luecke mit `mounted` ab. Disposen Sie jeden Controller. Kuendigen Sie jedes Abonnement.
Wenn Sie Screens mit komplexen Lifecycle-Interaktionen haben, unterstuetze ich gern mit einem Review auf potenzielle Leaks und Performance-Probleme.
Verwandte Artikel
Flutter State Management: Riverpod, Provider und Bloc im Vergleich
Vergleichen Sie State-Management-Ansätze in Flutter. Verstehen Sie Riverpod, Provider und Bloc mit klaren Entscheidungskriterien.
Flutter Performance-Optimierung: Vollständiger Leitfaden
Steigern Sie die Performance Ihrer Flutter-App systematisch. Lernen Sie Rebuild-Optimierung, Speichermanagement, Lazy Loading und Profiling.
Dart Best Practices: Sauberer und wartbarer Code
Lernen Sie praxiserprobte Dart-Methoden für lesbaren, testbaren und wartbaren Code.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen