Skip to Content
Mobile Apps4 - FlutterWidgets mit State

Widgets mit State

Dauer: 50 Minuten

  • stateful Widgets
  • Interaktion mit dem Zustand durch setState
  • Zustände in MyHomePage und TodoItem implementieren

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: