Flutter Widget Lifecycle: Von initState bis dispose

12 Min. Lesezeit9. Februar 2026Aktualisiert: 9. März 2026
Flutter lifecycleinitState disposeStatefulWidget lifecycleFlutter memory leakFlutter setState after disposeFlutter widget rebuildFlutter stateful widgetFlutter best practices

# 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.

dart
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.

dart
@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:

  • `super.initState()` muss immer als Erstes aufgerufen werden.
  • `context` kann hier nicht fuer InheritedWidget-Abfragen verwendet werden, da das Widget noch nicht vollstaendig in den Baum eingehaengt ist. Dafuer ist `didChangeDependencies` vorgesehen.
  • Die Methode muss synchron bleiben. Wenn asynchrone Arbeit noetig ist, rufen Sie eine async-Methode von hier aus auf, machen Sie `initState` selbst aber nicht async.
  • 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.

    dart
    @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.

    dart
    @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:

  • Niemals `setState` innerhalb von `build` aufrufen.
  • Keine I/O- oder Netzwerkanfragen hier durchfuehren.
  • Wenn aufwaendige Berechnungen noetig sind, das Ergebnis in einem Feld zwischenspeichern und in `setState` aktualisieren.
  • 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.

    dart
    @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.

    dart
    @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.

    dart
    @override
    void dispose() {
      _tabController.dispose();
      _scrollController.dispose();
      _animationController.dispose();
      _abonnement?.cancel();
      _focusNode.dispose();
      super.dispose();
    }

    Regeln:

  • `super.dispose()` muss immer als Letztes aufgerufen werden.
  • Niemals `setState` aufrufen, nachdem `dispose` gelaufen ist. Das ist die Ursache des beruehmten "setState() called after dispose()"-Fehlers.
  • Vollstaendiges Beispiel: Alle Lifecycle-Methoden zusammen

    dart
    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

    dart
    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.

    dart
    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.

    dart
    @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.

    dart
    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.

    dart
    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.

    dart
    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.

    dart
    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

  • Braucht das Widget veraenderlichen State? Nein -> **StatelessWidget**.
  • Wird dieser State von Riverpod verwaltet? Ja -> **ConsumerWidget** (oder **ConsumerStatefulWidget**, wenn auch Lifecycle-Methoden benoetigt werden).
  • Ist der State lokal zum Widget (Animationen, Controller, Formular-State)? Ja -> **StatefulWidget**.
  • 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.

    dart
    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.

    dart
    @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:

  • Ein Eltern-Widget aendert den `key` Ihres Widgets.
  • Die Position des Widgets im Baum aendert sich (z.B. es befindet sich in einem bedingten Block, der umschaltet).
  • Eine `PageView` oder `ListView` entfernt Off-Screen-Kinder aus dem Speicher.
  • 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:

    dart
    @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

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen