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

Flutter-Lebenszyklus – StatefulWidget und AppLifecycleState verwalten

Ahmet Balaman
FlutterLife CycleStatefulWidgetState ManagementApp Lifecycle

Der Lebenszyklus in Flutter

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.

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:

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

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();
  }
}

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 immer mounted (bei asynchronen Abläufen)
  2. Räumen Sie in dispose auf (Timer, Streams, Controller)
  3. Vergessen Sie die super-Aufrufe nicht
  4. Halten Sie initState schlank (nutzen Sie Future.delayed für schwere Vorgänge)
  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! 🚀

Kommentare