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

Flutter-Lebenszyklus: StatefulWidget und AppLifecycleState

Ahmet Balaman

Zuletzt aktualisiert:

5 Min. Lesezeit

FlutterLife CycleStatefulWidgetState ManagementApp Lifecycle
Flutter-Lebenszyklus: StatefulWidget und AppLifecycleState

Als ich das erste Mal einen API-Aufruf brauchte, fragte ich mich: „Wo schreibe ich den hin?“ In die Methode build()? Nein, denn die wird bei jedem Neuzeichnen aufgerufen. Genau hier kommt der Lebenszyklus ins Spiel.

In Flutter gibt es zwei Arten von Widgets: StatelessWidget und StatefulWidget. Ein StatelessWidget hat keinen eigenen Lebenszyklus (weil es sich nicht ändert), ein StatefulWidget dagegen einen sehr reichhaltigen. Falls der Unterschied zwischen beiden oder der Aufbau des Widget-Baums noch unklar ist, beginnen Sie am besten beim Beitrag zu Widget-Baum und Layout.

Live-Demo: die Lebenszyklus-Methoden

Probieren Sie die Lebenszyklus-Methoden eines StatefulWidget interaktiv aus:

💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.

Der Lebenszyklus eines StatefulWidget

Das Leben eines StatefulWidget verläuft so:

StatefulWidget-Lebenszyklus: Erzeugung mit createState, initState, didChangeDependencies und build; Aktualisierung mit setState und build; Abbau mit deactivate und dispose

createState() → initState() → didChangeDependencies() → build()
                                         ↓
                    setState() ←→ build() ←→ didUpdateWidget()
                                         ↓
                                    dispose()

1. createState()

Wird beim ersten Erzeugen des Widgets aufgerufen und legt das State-Objekt an.

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() {
    print('createState executed');
    return _MyWidgetState();
  }
}

Wann aufgerufen: Beim Erzeugen des Widgets, genau einmal.

Wofür: Um die State-Klasse zurückzugeben. Muss meist nicht angepasst werden.

2. initState()

Wird unmittelbar nach dem Erzeugen des State-Objekts aufgerufen. Hier findet die Grundeinrichtung statt.

class _MyWidgetState extends State<MyWidget> {
  int counter = 0;
  late Timer timer;
  
  @override
  void initState() {
    super.initState(); // Must call this!
    
    print('initState executed');
    
    // Initial setup
    counter = 0;
    _loadData();
    _startTimer();
  }
  
  void _loadData() async {
    // API call
    final data = await fetchDataFromAPI();
    setState(() {
      // Save data to state
    });
  }
  
  void _startTimer() {
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      setState(() {
        counter++;
      });
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Text('Counter: $counter');
  }
}

Wann aufgerufen: Beim ersten Erzeugen des Widgets, genau einmal.

Wofür:

  • Anfangswerte für Variablen setzen
  • API-Aufrufe starten
  • Streams und Controller anlegen
  • Timer starten
  • Event-Listener registrieren

Wichtig: Vergessen Sie den Aufruf von super.initState() nicht!

3. didChangeDependencies()

Wird aufgerufen, wenn sich die Abhängigkeiten des Widgets ändern.

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  
  print('didChangeDependencies executed');
  
  // Did theme change?
  final theme = Theme.of(context);
  
  // Did MediaQuery change? (screen size, orientation)
  final screenSize = MediaQuery.of(context).size;
}

Wann aufgerufen:

  • Direkt nach initState()
  • Wenn sich ein InheritedWidget ändert (Theme, MediaQuery usw.)

Wofür:

  • Vorgänge, die vom Context abhängen
  • Änderungen an Theme oder Locale erkennen

Hinweis: In initState() können Sie context nicht verwenden, hier aber schon.

4. build()

Erzeugt die Ansicht des Widgets.

@override
Widget build(BuildContext context) {
  print('build executed');
  
  return Scaffold(
    appBar: AppBar(title: Text('Page')),
    body: Center(
      child: Text('Counter: $counter'),
    ),
  );
}

Wann aufgerufen:

  • Nach didChangeDependencies()
  • Bei jedem Aufruf von setState()
  • Nach didUpdateWidget()
  • Wenn das übergeordnete Widget neu aufgebaut wird

Wofür: Um die Oberfläche zu erzeugen.

Wichtig:

  • Führen Sie hier keine API-Aufrufe aus!
  • Starten Sie hier keine Timer!
  • Die Methode kann häufig aufgerufen werden – achten Sie auf die Performance.

5. didUpdateWidget()

Wird aufgerufen, wenn sich das übergeordnete Widget ändert und eine neue Konfiguration schickt.

class ParentWidget extends StatefulWidget {
  final String title;
  
  ParentWidget({required this.title});
  
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  @override
  void didUpdateWidget(ParentWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    
    print('didUpdateWidget executed');
    
    // Compare old and new widget
    if (oldWidget.title != widget.title) {
      print('Title changed: ${oldWidget.title} → ${widget.title}');
      // Make necessary updates
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Text(widget.title);
  }
}

Wann aufgerufen: Wenn sich das übergeordnete Widget ändert und neu aufgebaut wird.

Wofür:

  • Altes und neues Widget vergleichen
  • Auf geänderte Parameter reagieren

6. setState()

Dient dazu, den Zustand zu aktualisieren und das Widget neu aufzubauen.

class _CounterState extends State<Counter> {
  int count = 0;
  
  void _increment() {
    setState(() {
      count++; // State change
    });
    // At this point, build() is called again
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          child: Text('Increment'),
          onPressed: _increment,
        ),
      ],
    );
  }
}

Wann aufgerufen: Sie rufen es selbst auf.

Wofür: Um Flutter über Zustandsänderungen zu informieren.

Wichtig:

  • Seien Sie bei asynchronen Abläufen vorsichtig
  • Vermeiden Sie unnötige setState-Aufrufe
  • Rufen Sie es nicht mehr auf, nachdem das Widget entfernt wurde

7. dispose()

Wird aufgerufen, bevor das Widget entfernt wird. Hier räumen wir auf.

class _MyWidgetState extends State<MyWidget> {
  late Timer timer;
  late StreamSubscription subscription;
  TextEditingController controller = TextEditingController();
  
  @override
  void initState() {
    super.initState();
    
    timer = Timer.periodic(Duration(seconds: 1), (timer) {
      // ...
    });
    
    subscription = someStream.listen((data) {
      // ...
    });
  }
  
  @override
  void dispose() {
    print('dispose executed - cleaning up');
    
    // Cancel timer
    timer.cancel();
    
    // Close stream
    subscription.cancel();
    
    // Dispose controller
    controller.dispose();
    
    super.dispose(); // Must call this last!
  }
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Wann aufgerufen: Bevor das Widget aus dem Widget-Baum entfernt wird.

Wofür:

  • Timer stoppen
  • Streams schließen
  • Controller freigeben
  • Event-Listener entfernen
  • Speicherlecks verhindern

Wichtig: super.dispose() muss zuletzt aufgerufen werden!

Praxisbeispiel: API-Aufruf im Lebenszyklus

Das folgende Muster ist ein von Hand geschriebener Ladeablauf: Die Daten werden in initState geholt und mit setState auf den Bildschirm gebracht. Wenn Sie dasselbe mit weniger Code möchten, übernimmt FutureBuilder die Zustände Laden, Fehler und Ergebnis für Sie; bei fortlaufend eintreffenden Daten passt StreamBuilder besser.

class UserProfilePage extends StatefulWidget {
  final int userId;
  
  UserProfilePage({required this.userId});
  
  @override
  _UserProfilePageState createState() => _UserProfilePageState();
}

class _UserProfilePageState extends State<UserProfilePage> {
  bool isLoading = true;
  String? userName;
  String? errorMessage;
  
  @override
  void initState() {
    super.initState();
    print('1. initState - Making API call');
    _loadUserData();
  }
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print('2. didChangeDependencies - Context ready');
  }
  
  @override
  void didUpdateWidget(UserProfilePage oldWidget) {
    super.didUpdateWidget(oldWidget);
    print('3. didUpdateWidget - Did userId change?');
    
    // Reload if userId changed
    if (oldWidget.userId != widget.userId) {
      setState(() {
        isLoading = true;
        errorMessage = null;
      });
      _loadUserData();
    }
  }
  
  Future<void> _loadUserData() async {
    try {
      print('API call started: userId=${widget.userId}');
      
      // Simulated API call
      await Future.delayed(Duration(seconds: 2));
      
      // Check if widget is still mounted
      if (!mounted) return;
      
      setState(() {
        userName = 'User ${widget.userId}';
        isLoading = false;
      });
      
      print('API call completed');
    } catch (e) {
      if (!mounted) return;
      
      setState(() {
        errorMessage = 'Error: $e';
        isLoading = false;
      });
    }
  }
  
  @override
  Widget build(BuildContext context) {
    print('4. build - Creating UI');
    
    return Scaffold(
      appBar: AppBar(title: Text('Profile')),
      body: Center(
        child: isLoading
            ? CircularProgressIndicator()
            : errorMessage != null
                ? Text(errorMessage!)
                : Text('Hello, $userName!'),
      ),
    );
  }
  
  @override
  void dispose() {
    print('5. dispose - Cleaning up widget');
    super.dispose();
  }
}

Beachten Sie dabei: Dieser Zustand verschwindet mit dispose, sobald die Seite per pop geschlossen wird. Für Daten, die sich mehrere Bildschirme teilen und die einen Seitenwechsel überleben müssen, greifen Sie statt zum Widget-State zu einer Lösung wie Provider oder Riverpod.

Der Lebenszyklus der Anwendung

Anders als der Lebenszyklus eines Widgets hat auch die Anwendung selbst einen Lebenszyklus.

AppLifecycleState

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }
  
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    
    switch (state) {
      case AppLifecycleState.resumed:
        print('App active - User in app');
        // Update data, start animations
        break;
        
      case AppLifecycleState.inactive:
        print('App inactive - Temporary state');
        // Phone call, notification pull down
        break;
        
      case AppLifecycleState.paused:
        print('App in background');
        // Save data, stop operations
        break;
        
      case AppLifecycleState.detached:
        print('App terminating');
        // Final cleanup
        break;
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: HomePage());
  }
}

Beschreibung der Zustände

  • resumed: Die App ist sichtbar und die Nutzerin interagiert damit
  • inactive: Die App ist sichtbar, aber ohne Interaktion (etwa bei einem Anruf)
  • paused: Die App läuft im Hintergrund und ist nicht sichtbar
  • detached: Die App wird beendet

Praxisbeispiel: Videoplayer

class VideoPlayerPage extends StatefulWidget {
  @override
  _VideoPlayerPageState createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage>
    with WidgetsBindingObserver {
  late VideoPlayerController videoController;
  
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    
    videoController = VideoPlayerController.network('video_url');
    videoController.initialize();
    videoController.play();
  }
  
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      // App moved to background - pause video
      videoController.pause();
    } else if (state == AppLifecycleState.resumed) {
      // App active - resume video
      videoController.play();
    }
  }
  
  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    videoController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: AspectRatio(
          aspectRatio: videoController.value.aspectRatio,
          child: VideoPlayer(videoController),
        ),
      ),
    );
  }
}

Die Methode deactivate()

Wird selten genutzt und aufgerufen, wenn das Widget vorübergehend aus dem Baum entfernt wird.

@override
void deactivate() {
  super.deactivate();
  print('Widget deactivated');
  // Temporary cleanup operations
}

Wann aufgerufen:

  • Bei Seitenwechseln über den Navigator
  • Wenn das Widget im Baum umgehängt wird

Häufige Fehler

1. Context in initState verwenden

// ❌ Wrong
@override
void initState() {
  super.initState();
  final theme = Theme.of(context); // ERROR!
}

// ✅ Correct
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  final theme = Theme.of(context); // Correct
}

2. setState nach dispose

// ❌ Wrong
Future<void> loadData() async {
  final data = await api.fetchData();
  setState(() {
    this.data = data; // Widget might be disposed!
  });
}

// ✅ Correct
Future<void> loadData() async {
  final data = await api.fetchData();
  if (mounted) { // mounted check
    setState(() {
      this.data = data;
    });
  }
}

3. Seiteneffekte in build()

// ❌ Wrong
@override
Widget build(BuildContext context) {
  fetchData(); // Called on every build!
  return Text('...');
}

// ✅ Correct
@override
void initState() {
  super.initState();
  fetchData(); // Called once
}

4. Den super-Aufruf vergessen

// ❌ Wrong
@override
void initState() {
  // No super.initState()!
  loadData();
}

// ✅ Correct
@override
void initState() {
  super.initState(); // Must call this!
  loadData();
}

Best Practices

  1. Prüfen Sie nach asynchronen Abläufen immer mounted.
  2. Räumen Sie Timer, Stream-Abos und Controller in dispose auf.
  3. Vergessen Sie die super-Aufrufe nicht.
  4. Halten Sie initState schlank und verschieben Sie schwere Arbeit.
  5. Halten Sie build() frei von Seiteneffekten.

Zusammenfassung

Lebenszyklus eines StatefulWidget:

  1. createState() – State erzeugen
  2. initState() – Grundeinrichtung
  3. didChangeDependencies() – Änderung der Abhängigkeiten
  4. build() – Oberfläche erzeugen
  5. didUpdateWidget() – Änderung im Elternteil
  6. setState() – Zustand aktualisieren
  7. dispose() – aufräumen

Lebenszyklus der App:

  • resumed – aktiv
  • inactive – vorübergehend inaktiv
  • paused – im Hintergrund
  • detached – wird beendet

Die Lebenszyklus-Methoden zu verstehen ist grundlegend, um performante und fehlerfreie Flutter-Anwendungen zu schreiben.


Beim Lebenszyklus ins Stocken geraten?

Bis zum nächsten Artikel! 🚀

Häufig gestellte Fragen

Gehört der API-Aufruf in initState?

Ja – für ein einmaliges Laden ist initState der richtige Ort. In build() gehört er nie, denn diese Methode läuft bei jedem Neuzeichnen. Statt in initState direkt await zu verwenden, rufen Sie eine async-Methode auf und schreiben das Ergebnis mit setState.

Was genau muss ich in dispose aufräumen?

Alles, was Sie selbst erzeugt haben: TextEditingController, AnimationController, ScrollController, jeden Timer und jede StreamSubscription. Bleiben sie bestehen, laufen sie weiter, nachdem das Widget den Baum verlassen hat, und verursachen Speicherlecks.

Wie vermeide ich „setState() called after dispose()“?

Dieser Fehler bedeutet, dass ein asynchroner Ablauf erst fertig wurde, als es das Widget nicht mehr gab. Sichern Sie den Aufruf mit if (!mounted) return; vor setState ab und brechen Sie den Vorgang oder das Abo nach Möglichkeit in dispose ab.

Kommentare