Widgets mit State
Dauer: 50 Minuten
statefulWidgets- Interaktion mit dem Zustand durch
setState - Zustände in
MyHomePageundTodoItemimplementieren
Ziel: Widgets und State in Flutter verstehen
Vorstellung des Frameworks
- Installation und Einrichtung der Entwicklungsumgebung
- Prinzipien des Frameworks (Projektstruktur, Komponenten/Widgets, …)
- App mit einem Screen: UI-Elemente, Layout, Interaktion und State
- Daten lokal speichern
- Navigation zwischen mehreren Screens
In Android hatten wir es mit Composables zu tun, in Flutter heißen die UI-Komponenten Widgets. Während in Compose einzelne Composable-Funktionen mehrere Zustandsobjekte haben können, sind in Flutter die Widgets durch die Klassenhierarchie als Ganzes stateless oder stateful. Trotz dieses Unterschieds gibt es viele Parallelen zwischen den beiden Frameworks. So führt in beiden Ansätzen eine Änderung des Zustands zu einer automatischen Aktualisierung des UIs.
Stateful Widgets
Ein stateful Widget ist eine Klasse, die von StatefulWidget
erbt und die Methode createState implementiert. Da ein stateful
Widget selbst unveränderlich ist, speichert es veränderlichen Zustand
in einer separaten Klasse, die eine Subklasse von State ist.
Diese State-Klasse wird im stateful Widget von der Methode
createState zurückgegeben.
Ein stateful Widget ist eine Klasse, die von StatefulWidget erbt und
createState() implementiert. Das Widget selbst ist unveränderlich;
veränderlicher Zustand liegt in einer separaten State-Subklasse, die
createState() als State<MyWidget> zurückgibt. Die UI wird in der
State-Klasse (nicht im StatefulWidget) in build() aufgebaut; Änderungen
an der Darstellung löst man über setState() in der State-Klasse aus.
Wir bauen TodoItem als stateful Widget um:
Achtung: Wir können die build-Methode zunächst auskommentieren,
denn diese wird gleich in den State verschoben.
class TodoItem extends StatefulWidget {
final String title;
const TodoItem({super.key, required this.title});
@override
State<TodoItem> createState() => _TodoItemState();
// Achtung: build-Methode gehört nun in den State!
// vorübergehend auskommentiert:
/* @override
Widget build(BuildContext context) {
// Rest des Codes bleibt unverändert
}
*/
}Die Methode createState() wird einmal beim Einfügen des Widgets
in den UI-Baum aufgerufen und liefert eine Instanz von State<MyWidget>,
die den veränderlichen Zustand verwaltet. StatefulWidget selbst hat
keine build()-Methode; die UI wird in der State-Klasse über build()
aufgebaut. Auf die aktuelle Widget-Konfiguration greifen wir in State über
widget zu (z.B. bei uns widget.title). Dazu implementieren wir unterhalb
von TodoItem die Klasse _TodoItemState, die von State<TodoItem> erbt:
Achtung: Wir können die build-Methode nun in die
_TodoItemState-Klasse verschieben, müssen aber im Text-Widget
auf widget.title zugreifen (anstatt nur title wie zuvor).
import 'package:flutter/material.dart';
class TodoItem extends StatefulWidget {
final String title;
const TodoItem({super.key, required this.title});
@override
State<TodoItem> createState() => _TodoItemState();
}
class _TodoItemState extends State<TodoItem> {
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.bodyLarge;
return CheckboxListTile(
value: false,
onChanged: (bool? value) {
/* to be implemented */
},
title: Text(
widget.title, // Achtung: Zugriff auf widget.title!
style: textStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
controlAffinity: ListTileControlAffinity.leading,
);
}
}Es ist üblich, den Zustand in einer separaten Klasse zu
speichern, die mit einem Unterstrich beginnt und den Zusatz State
im Namen trägt. Der führende Unterstrich _ macht die State-Klasse
library-private In unserem Fall ist dies _TodoItemState. Der
Zustand kann dann in der State-Klasse verwaltet werden.
Interaktion mit dem Zustand durch setState
Das State-Objekt kann den Zustand des Widgets ändern, indem es
die Methode setState aufruft. Diese Methode nimmt eine Funktion
als Argument, die den neuen Zustand berechnet. In unserem Beispiel
wollen wir die Checkbox-Interaktion und das Antippen des Todos
implementieren, um den Zustand des TodoItem zu ändern.
Zunächst fügen wir im _TodoItemState eine _done-Instanzvariable vom
Typ bool hinzu, die den Zustand des Todos repräsentiert:
class _TodoItemState extends State<TodoItem> {
bool _done = false; // Instanzvariable für den Zustand
// restlicher Code bleibt unverändert
}Nun können wir den Handler für die Interaktionen implementieren,
dies ist onChanged in der CheckboxListTile. Mit der setState-Methode
können wir den Zustand des Todos ändern:
setState(() {
_done = !_done;
});setState wird mit einer Funktion aufgerufen, die den neuen Zustand
berechnet. In unserem Fall wird _done auf den negierten Wert von
_done gesetzt. Das führt dazu, dass der Zustand des Todos umgekehrt
wird, wenn die CheckboxListTile angetippt wird.
Außerdem müssen wir den aktuellen Wert der CheckboxListTile (value)
auf den des Zustands _done setzen.
Hier der relevante Code des _TodoItemState:
class _TodoItemState extends State<TodoItem> {
bool _done = false; // Instanzvariable für den Zustand
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.bodyLarge;
return CheckboxListTile(
value: _done,
onChanged: (bool? value) {
setState(() {
_done = !_done; // Zustand umkehren
});
},
// ... restlicher Code ...
);
}
}Die Änderung des Zustands bzw. der Aufruf von setState führt
zu einer automatischen Neuzeichnung des UIs — ganz ähnlich
wie in der nativen Android-Entwicklung mit Composables.
Wiederverwendbarkeit und Datenfluss nach außen.
Ähnlich wie das state hoisting in Android mit Compose heißt es in Flutter
lifting state up: Das Child meldet Änderungen per Callback (z. B.
onChanged) nach oben; der Parent hält die Single Source of Truth,
aktualisiert/speichert/synchronisiert das Modell. Das erhöht
Wiederverwendbarkeit und Testbarkeit und sorgt in Listen (Reorder/Animated)
für robuste Updates ohne lokalen State-Verlust.
Als Vorbereitung für die TodoListe bauen wir im State auch
initState und didUpdateWidgetein:
import 'package:flutter/material.dart';
class TodoItem extends StatefulWidget {
final String title;
final bool initialDone;
final ValueChanged<bool>? onChanged;
const TodoItem({
super.key,
required this.title,
this.initialDone = false,
this.onChanged,
});
@override
State<TodoItem> createState() => _TodoItemState();
}
class _TodoItemState extends State<TodoItem> {
late bool _done;
@override
void initState() {
super.initState();
_done = widget.initialDone;
}
@override
void didUpdateWidget(covariant TodoItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialDone != widget.initialDone) {
_done = widget.initialDone; // kein setState nötig
}
}
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.bodyLarge;
return CheckboxListTile(
value: _done,
onChanged: (v) {
if (v == null) return;
setState(() => _done = v);
widget.onChanged?.call(v);
},
// Rest des Codes bleibt unverändert
);
}
}MyHomePage in stateful Widget umwandeln
Wir wandeln nun MyHomePage in ein stateful Widget um, weil
wir dort die Todo-Liste als Zustand verwalten wollen.
In VS Code gibt es einen Shortcut, um ein Widget in ein stateful Widget umzuwandeln:
Cursor auf den Klassennamen setzen und dann Ctrl + . (Windows/Linux)
oder Cmd + . (Mac) drücken, sodass passende Code-Aktionen angezeigt
werden. Dort Convert to StatefulWidget auswählen.
(Die klappt wohl auch in Android Studio mit Alt + Enter)
Android Studio / IntelliJ IDEA: Platzieren Sie den Cursor auf dem
Der angepasste Code von MyHomePage und der neuen State-Klasse
_MyHomePageState sieht dann so aus:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Todo-App')),
body: const TodoList(),
floatingActionButton: FloatingActionButton(
onPressed: () => debugPrint('Todo hinzufügen'),
tooltip: 'Todo hinzufügen',
child: const Icon(Icons.add),
),
);
}
}TodoList-Zustand verwalten
TODO: Bisher nur der Code, Erklärung folgt noch
Neues Datenmodell für Todo in lib/todo.dart:
class Todo {
final String title;
final bool done;
const Todo({required this.title, this.done = false});
Todo copyWith({String? title, bool? done}) =>
Todo(title: title ?? this.title, done: done ?? this.done);
}Änderungen in todo_list.dart:
import 'package:flutter/material.dart';
import 'package:todos_flutter_2025/todo.dart';
import 'package:todos_flutter_2025/todo_item.dart';
class TodoList extends StatelessWidget {
final List<Todo> todos;
final void Function(int index, bool value) onChanged;
// der Konstruktor Argumente für die Todos und die Callback-Funktion
const TodoList({super.key, required this.todos, required this.onChanged});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, i) {
final t = todos[i];
return TodoItem(
key: ValueKey(i),
title: t.title,
initialDone: t.done,
onChanged: (v) => onChanged(i, v),
);
},
);
}
}Hier verwenden wir nun die todos-Instanzvariable im ListView.builder,
um die Todos zu rendern, was insbesondere dann benötigt wird, wenn
die Todo-Liste dynamisch ist. Mehr zu ListView in der Dokumentation:
API-Docs zu ListView
Schließlich muss main.dart angepasst werden, um den Zustand der Todo-Liste
zu verwalten:
import 'package:flutter/material.dart';
import 'package:todos_flutter_2025/todo.dart';
import 'package:todos_flutter_2025/todo_list.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todos',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final List<Todo> _todos = [];
void _addTodo() {
setState(() {
_todos.add(Todo(title: 'Neues Todo'));
});
}
void _toggle(int index, bool value) {
setState(() {
_todos[index] = _todos[index].copyWith(done: value);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Todo-App')),
body: TodoList(todos: _todos, onChanged: _toggle),
floatingActionButton: FloatingActionButton(
onPressed: _addTodo,
tooltip: 'Todo hinzufügen',
child: const Icon(Icons.add),
),
);
}
}AlertDialog für neue Todos
Zum Hinzufügen von neuen Todos können wir ein AlertDialog verwenden,
das eine Texteingabe für den Titel des neuen Todos enthält. Dazu
müssen wir den onPressed-Handler des FloatingActionButtons anpassen:
class _MyHomePageState extends State<MyHomePage> {
final List<Todo> _todos = [];
final TextEditingController _textController = TextEditingController();
@override
void dispose() {
_textController.dispose();
super.dispose();
}
void _addTodo() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Neues Todo'),
content: TextField(
controller: _textController,
autofocus: true,
decoration: const InputDecoration(hintText: 'Todo-Text eingeben'),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
_textController.clear();
},
child: const Text('Abbrechen'),
),
TextButton(
onPressed: () {
if (_textController.text.isNotEmpty) {
setState(() {
_todos.add(Todo(title: _textController.text));
});
_textController.clear();
Navigator.of(context).pop();
}
},
child: const Text('Hinzufügen'),
),
],
);
},
);
}
// ... restlicher Code ...
}Auch hier haben wir wieder eine starke Ähnlichkeit zu dem Code, den wir für die native Android-App in Compose geschrieben haben.
Zusammenfassung
In Flutter sind Widgets entweder stateless oder stateful.
Ein stateful Widget speichert seinen Zustand in einer separaten
State-Klasse, die von State erbt. Der Zustand kann durch
setState geändert werden, was zu einer automatischen
Neuzeichnung des UIs führt.
Mehr zu stateful Widgets in der Flutter-Dokumentation: