Flutter-Lebenszyklus – StatefulWidget und AppLifecycleState verwalten
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
- Prüfen Sie immer mounted (bei asynchronen Abläufen)
- Räumen Sie in dispose auf (Timer, Streams, Controller)
- Vergessen Sie die super-Aufrufe nicht
- Halten Sie initState schlank (nutzen Sie Future.delayed für schwere Vorgänge)
- Halten Sie build() frei von Seiteneffekten
Zusammenfassung
Lebenszyklus eines StatefulWidget:
createState()– State erzeugeninitState()– GrundeinrichtungdidChangeDependencies()– Änderung der Abhängigkeitenbuild()– Oberfläche erzeugendidUpdateWidget()– Änderung im ElternteilsetState()– Zustand aktualisierendispose()– aufräumen
Lebenszyklus der App:
resumed– aktivinactive– vorübergehend inaktivpaused– im Hintergrunddetached– 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! 🚀