Skip to Content
Folienvorschau
Dauer
10 Minuten
Ziel
Verstehen, wie Flutter auf Zustandsänderungen reagiert

Themen

  • StatefulWidget und State
  • setState
  • Lifting State Up
Klicken für Tastaturnavigation · F für Vollbild
Präsentation starten · 10 Minuten ↗

State

Bisher zeigt die App nur feste Daten an. Jetzt soll die Oberfläche auf Eingaben reagieren: Ein Umschalter in der AppBar wechselt die Temperaturanzeige zwischen °C und °F.

Code: schritt-05 · Änderungen 

Erster Versuch

Eine normale Variable reicht nicht: Antippen ändert den Wert, aber die Anzeige bleibt gleich.

@override Widget build(BuildContext context) { var useCelsius = true; // funktioniert nicht! return TextButton( onPressed: () => useCelsius = !useCelsius, child: Text(useCelsius ? '°C' : '°F'), ); }

Flutter weiß nicht, dass sich useCelsius geändert hat, und ruft build deshalb nicht erneut auf. Selbst wenn, stünde useCelsius danach wieder auf true, weil build von vorne ausgeführt wird.

Auch ein Feld im StatelessWidget hilft nicht: Widgets sind unveränderlich, der Analyzer warnt bei Feldern ohne final (must_be_immutable).

StatefulWidget

Ein StatefulWidget besteht aus zwei Klassen: dem Widget und seinem State, der über build hinweg erhalten bleibt.

class WeatherScreen extends StatefulWidget { const WeatherScreen({super.key, required this.city, required this.weather}); // final-Felder city und weather ... @override State<WeatherScreen> createState() => _WeatherScreenState(); } class _WeatherScreenState extends State<WeatherScreen> { bool _useCelsius = true; // build(...) wie bisher, Zugriff auf Felder des Widgets mit widget.city }

Den Umbau von WeatherScreen machen wir einmal von Hand, um zu sehen, was dabei passiert.

1. WeatherScreen erbt von StatefulWidget statt von StatelessWidget und bekommt die Methode createState. Die build-Methode zunächst ausschneiden, sie zieht gleich um:

lib/ui/weather_screen.dart
class WeatherScreen extends StatefulWidget { const WeatherScreen({super.key, required this.city, required this.weather}); final City city; final CurrentWeather weather; @override State<WeatherScreen> createState() => _WeatherScreenState(); }

Ein StatefulWidget hat selbst keine build-Methode. createState ruft Flutter einmal auf, wenn das Widget in den Baum kommt.

2. Darunter die Klasse _WeatherScreenState anlegen und build dort einfügen:

lib/ui/weather_screen.dart
class _WeatherScreenState extends State<WeatherScreen> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.city.name)), body: SingleChildScrollView( child: CurrentWeatherCard(city: widget.city, weather: widget.weather), ), ); } }

3. city und weather sind jetzt Felder einer anderen Klasse. Der Analyzer meldet deshalb Fehler, bis jeder Zugriff widget. voranstellt: widget.city, widget.weather. widget ist das aktuelle Widget, zu dem dieser State gehört.

4. Erst jetzt kommt der eigentliche Zustand dazu: das Feld bool _useCelsius = true; in _WeatherScreenState. Wie es geändert wird, zeigt der nächste Abschnitt.

Der Unterstrich in _WeatherScreenState und _useCelsius macht beides privat für die Datei. Das Widget selbst bleibt unveränderlich. Den veränderlichen Zustand hält das State-Objekt, das Flutter einmal erzeugt und bei jedem neuen build wiederverwendet. Damit übernimmt es die Rolle von remember in Compose.

Wer den Umbau einmal von Hand gemacht hat, kann künftig den Quick Fix nutzen: Cursor auf StatelessWidget, ⌘ . (Windows/Linux: Strg + .) und Convert to StatefulWidget. Er erledigt die Schritte 1 bis 3.

setState

setState ändert den Zustand und sagt Flutter, dass build erneut aufgerufen werden muss.

TextButton( onPressed: () => setState(() => _useCelsius = !_useCelsius), child: Text(_useCelsius ? '°C' : '°F'), )

Merksatz wie bei Compose: UI = f(State). Die Oberfläche ist eine Funktion des Zustands; ändert sich der State, wird sie neu berechnet.

Das erneute Bauen lässt sich beobachten: in build von CurrentWeatherCard die Zeile debugPrint('build CurrentWeatherCard'); einfügen und die Debug Console von VS Code öffnen. Jedes Umschalten erzeugt eine neue Zeile. Anders als Compose überspringt Flutter dabei nicht automatisch unveränderte Kinder: setState baut den ganzen Teilbaum unterhalb dieses State neu. Ausgenommen sind nur const-Widgets. Das klingt teuer, ist es aber nicht: Widgets sind leichte Beschreibungen, gezeichnet wird nur, was sich wirklich geändert hat.

Beim Drehen des Bildschirms bleibt der State erhalten, ein Gegenstück zu rememberSaveable braucht es dafür nicht. Flutter verarbeitet die Drehung selbst, statt die Activity neu zu erzeugen. Im Emulator ausprobieren: auf °F schalten und das Gerät drehen.

Lifting State Up

Den State dort halten, wo alle ihn brauchen, und ihn nach unten weitergeben. Änderungen kommen als Callbacks zurück nach oben.

Unidirectional Data Flow: State nach unten, Events nach oben

Der Umschalter (UnitToggle) und die Karte (CurrentWeatherCard) brauchen beide useCelsius. Deshalb liegt der State im gemeinsamen Eltern-Widget WeatherScreen. In Compose heißt das Muster State Hoisting, in Flutter Lifting State Up; das Prinzip dahinter ist dasselbe: State fließt nach unten, Events nach oben.

bool _useCelsius = true; UnitToggle( useCelsius: _useCelsius, // State ↓ onToggle: () => setState(() => _useCelsius = !_useCelsius), // Event ↑ ), CurrentWeatherCard(city: …, weather: …, useCelsius: _useCelsius), // State ↓

Vollständig in _WeatherScreenState: UnitToggle steckt in den actions der AppBar, die Karte im body des Scaffold.

lib/ui/weather_screen.dart
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.city.name), actions: [ UnitToggle( useCelsius: _useCelsius, onToggle: () => setState(() => _useCelsius = !_useCelsius), ), ], ), body: SingleChildScrollView( child: CurrentWeatherCard( city: widget.city, weather: widget.weather, useCelsius: _useCelsius, ), ), ); }

CurrentWeatherCard bekommt dafür ein neues Feld useCelsius und zeigt die Temperatur mit formatTemperature an (siehe unten):

Code: CurrentWeatherCard in lib/ui/weather_screen.dart (schritt-05)

lib/ui/weather_screen.dart
class CurrentWeatherCard extends StatelessWidget { const CurrentWeatherCard({ super.key, required this.city, required this.weather, required this.useCelsius, }); final City city; final CurrentWeather weather; final bool useCelsius; @override Widget build(BuildContext context) { return Container( width: double.infinity, margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color(0xFFBBDEFB), borderRadius: BorderRadius.circular(24), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(city.name, style: const TextStyle(fontSize: 28)), Text(weather.condition.emoji, style: const TextStyle(fontSize: 72)), Text( formatTemperature(weather.temperature, useCelsius: useCelsius), style: const TextStyle(fontSize: 48), ), const SizedBox(height: 16), Row( children: [ Text('Wind: ${weather.windSpeed.round()} km/h'), const Spacer(), Text(weather.condition.label), ], ), ], ), ); } }

UnitToggle steht in derselben Datei, zwischen _WeatherScreenState und CurrentWeatherCard. Es ist stateless, bekommt den aktuellen Wert und meldet das Antippen über onToggle:

lib/ui/weather_screen.dart
class UnitToggle extends StatelessWidget { const UnitToggle({super.key, required this.useCelsius, required this.onToggle}); final bool useCelsius; final VoidCallback onToggle; @override Widget build(BuildContext context) { return TextButton( onPressed: onToggle, child: Text(useCelsius ? '°C' : '°F'), ); } }

VoidCallback ist der Typ einer Funktion ohne Parameter und Rückgabewert, wie () -> Unit in Kotlin.

Die Umrechnung steckt in einer Hilfsfunktion am Ende der Datei, außerhalb der Extension:

lib/ui/weather_condition_ui.dart
String formatTemperature(double celsius, {required bool useCelsius}) => useCelsius ? '${celsius.round()} °C' : '${(celsius * 9 / 5 + 32).round()} °F';

useCelsius ist hier ein benannter Parameter. So ist beim Aufruf klar, was true bedeutet.

Übung: Zähler

In DartPad  einen Zähler bauen:

  • eine große Zahl, darunter Buttons „+1“, „+10“ und „Zurücksetzen“
  • Bonus: Button „−1“, der nicht unter 0 zählt
  • Bonus: die Buttons in ein eigenes StatelessWidget CounterButtons auslagern

DartPad öffnet mit einem Dart-Beispiel; über Samples gibt es Flutter-Beispiele, deren Code wir ersetzen. Die Vorschau erscheint rechts. Übrigens: Die Vorlage Application von Flutter: New Project ist genau so ein Zähler.

Bei der zweiten Bonus-Aufgabe kommt Lifting State Up ins Spiel: Der State bleibt im StatefulWidget, CounterButtons bekommt nur Callbacks.

Lösung

main.dart
import 'dart:math'; import 'package:flutter/material.dart'; void main() { runApp(const MaterialApp(home: CounterScreen())); } class CounterScreen extends StatefulWidget { const CounterScreen({super.key}); @override State<CounterScreen> createState() => _CounterScreenState(); } class _CounterScreenState extends State<CounterScreen> { int _count = 0; void _add(int amount) { setState(() => _count = max(0, _count + amount)); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('$_count', style: const TextStyle(fontSize: 96)), CounterButtons( onAdd: _add, onReset: () => setState(() => _count = 0), ), ], ), ), ); } } class CounterButtons extends StatelessWidget { const CounterButtons({super.key, required this.onAdd, required this.onReset}); final ValueChanged<int> onAdd; final VoidCallback onReset; @override Widget build(BuildContext context) { return Wrap( spacing: 8, children: [ FilledButton(onPressed: () => onAdd(-1), child: const Text('−1')), FilledButton(onPressed: () => onAdd(1), child: const Text('+1')), FilledButton(onPressed: () => onAdd(10), child: const Text('+10')), OutlinedButton(onPressed: onReset, child: const Text('Zurücksetzen')), ], ); } }

max aus dart:math verhindert negative Werte. ValueChanged<int> ist der Typ eines Callbacks mit einem int-Parameter, wie (Int) -> Unit in Kotlin. _add passt dazu und wird ohne Klammern übergeben. Wrap setzt die Buttons nebeneinander und bricht um, wenn der Platz nicht reicht.

Zusammenfassung

State in Flutter: StatefulWidget mit einem State-Objekt, das den Wert behält, setState zum Ändern, State so weit oben wie nötig, Events als Callbacks nach oben.

ComposeFlutter
remember { mutableStateOf(…) }Feld im State eines StatefulWidget
Zuweisung useCelsius = …setState(() => _useCelsius = …)
Recompositionerneuter Aufruf von build
rememberSaveablebeim Drehen nicht nötig
State HoistingLifting State Up
() -> UnitVoidCallback

Für einen einzelnen Schalter reicht setState. In der nächsten Lektion wandert der Zustand des ganzen Screens eine Ebene höher: in ein ViewModel.

Weiterführendes Material