Flutter: Eingabe-Widgets und Statusanzeigen
Zuletzt aktualisiert:
7 Min. Lesezeit

Eingaben vom Nutzer zu bekommen heißt nicht immer, die Tastatur zu öffnen. Für eine Ein/Aus-Einstellung nehmen wir einen Switch, für Mehrfachauswahl eine Checkbox, für eine Einzelauswahl Radio und für einen Wert aus einem Bereich einen Slider; läuft ein Vorgang, informiert eine Fortschrittsanzeige den Nutzer. Dieser Beitrag behandelt alle diese Widgets, erklärt, welches in welcher Situation die richtige Wahl ist, und zeigt die häufigsten Stolperfallen.
Live-Demo
Sie können diese Widgets im interaktiven Beispiel unten ausprobieren:
💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.
Auswahl-Widgets
Alle diese Widgets folgen derselben Logik: Sie speichern den Wert nicht selbst. Wir übergeben den aktuellen Wert in value, der Nutzer berührt das Widget, onChanged meldet den neuen Wert, und wir aktualisieren unsere Variable mit setState, damit der Bildschirm neu gezeichnet wird.
1. Switch
Schaltet eine Einstellung ein oder aus. Die Änderung wirkt sofort; sie wartet nicht auf einen „Speichern“-Button.
bool isSwitched = false;
Switch(
value: isSwitched,
onChanged: (value) {
setState(() {
isSwitched = value;
});
},
)Auf echten Bildschirmen taucht er fast immer zusammen mit seiner Beschriftung auf, als SwitchListTile. Dadurch wird die gesamte Zeile antippbar:
SwitchListTile(
title: const Text('Benachrichtigungen aktivieren'),
subtitle: const Text('App-Benachrichtigungen erhalten'),
value: isSwitched,
onChanged: (value) => setState(() => isSwitched = value),
)2. Checkbox
Dient dazu, mehrere voneinander unabhängige Optionen anzuhaken oder etwas zu bestätigen.
bool isChecked = false;
Checkbox(
value: isChecked,
onChanged: (value) {
setState(() {
isChecked = value ?? false;
});
},
)Der Parameter von onChanged hat den Typ bool?, weil eine Checkbox mit tristate: true auch einen dritten, „unbestimmten“ Zustand (null) anzeigen kann. Er ist für Fälle wie „einige Unterelemente sind ausgewählt“ gedacht. Für die beschriftete Variante ist CheckboxListTile praktischer.
3. Radio
Dient dazu, aus einer Gruppe genau eine Option zu wählen. In aktuellen Flutter-Versionen verwaltet ein RadioGroup den ausgewählten Wert der Gruppe:
int selectedValue = 1;
RadioGroup<int>(
groupValue: selectedValue,
onChanged: (value) => setState(() => selectedValue = value ?? 1),
child: const Column(
children: [
RadioListTile<int>(title: Text('Option A'), value: 1),
RadioListTile<int>(title: Text('Option B'), value: 2),
],
),
)In älteren Beispielen werden groupValue und onChanged jedem RadioListTile einzeln übergeben. Das kompiliert weiterhin, erzeugt in neueren Versionen aber eine Deprecation-Warnung; in neuem Code sollten Sie RadioGroup verwenden.
4. Slider
Dient dazu, einen Zahlenwert aus einem Bereich zu wählen: Lautstärke, Schriftgröße, Preisobergrenze.
double currentSliderValue = 20;
Slider(
value: currentSliderValue,
min: 0,
max: 100,
divisions: 5,
label: currentSliderValue.round().toString(),
onChanged: (double value) {
setState(() {
currentSliderValue = value;
});
},
)Ohne divisions liefert der Slider stufenlose Werte. Im Standard-Theme erscheint die label-Blase nur, wenn divisions gesetzt ist; das ist meist die Antwort auf „Ich habe ein Label gesetzt, aber es wird nie angezeigt“. Um Unter- und Obergrenze gemeinsam zu wählen, gibt es RangeSlider, der nach demselben Prinzip arbeitet.
5. ToggleButtons und SegmentedButton
ToggleButtons ist eine Gruppe nebeneinander angeordneter Buttons. Ob ein Button ausgewählt ist, halten wir in einer List<bool> fest – das passt gut zu unabhängigen Optionen wie fett/kursiv/unterstrichen:
List<bool> isSelected = [false, true, false];
ToggleButtons(
isSelected: isSelected,
onPressed: (int index) {
setState(() {
isSelected[index] = !isSelected[index];
});
},
children: const [
Icon(Icons.format_bold),
Icon(Icons.format_italic),
Icon(Icons.format_underlined),
],
)In Material 3 übernimmt SegmentedButton dieselbe Aufgabe. Er hält die Auswahl als Set der tatsächlichen Werte statt als Liste von Booleans, was vor allem mit Enums deutlich lesbarer ist. Im Szenario mit dem Einstellungsbildschirm weiter unten setzen wir ihn ein.
Fortschrittsanzeigen
Sie verhindern, dass der Bildschirm eingefroren wirkt, während etwas passiert. Es gibt zwei Betriebsarten: Ohne value dreht sich die Anzeige endlos (unbestimmt); mit einem value zwischen 0.0 und 1.0 ist sie zu diesem Anteil gefüllt (bestimmt).
1. CircularProgressIndicator
Wird eingesetzt, während eine Seite oder Liste lädt und die Dauer unbekannt ist.
// Unbestimmt (dreht sich fortlaufend)
const CircularProgressIndicator()
// Bestimmt (zu 50 % gefüllt)
const CircularProgressIndicator(value: 0.5)Typischer Einsatzort ist ein Bildschirm, der auf Daten wartet; in einem FutureBuilder dreht sich diese Anzeige in der Mitte, solange die Verbindung aussteht.
2. LinearProgressIndicator
Wird dort eingesetzt, wo sich Fortschritt messen lässt, etwa bei einem Datei-Download oder einem mehrstufigen Vorgang – meist am oberen Bildschirmrand oder am unteren Rand einer Karte.
LinearProgressIndicator(value: downloaded / total)Wenn sich ein Prozentwert berechnen lässt, sollten Sie den bestimmten Modus bevorzugen. „Wie viel fehlt noch“ beruhigt Nutzer deutlich mehr als ein kreisender Ring.
Kurzer Hinweis zu WebView
Weil es in diesem Zusammenhang oft gefragt wird: Um eine Webseite innerhalb der App anzuzeigen, verwenden Sie das vom Flutter-Team entwickelte Paket webview_flutter. Sie fügen es mit flutter pub add webview_flutter hinzu, erzeugen einen WebViewController und bringen ihn mit WebViewWidget(controller: controller) auf den Bildschirm. Das eignet sich für Inhalte, die ohnehin im Web liegen, etwa eine Datenschutzerklärung; die eigentlichen Bildschirme der App per WebView zu bauen, führt den Einsatz von Flutter ad absurdum.
Wann verwenden – und wann nicht?
| Bedarf | Richtiges Widget | Falsche Wahl |
|---|---|---|
| Ein/Aus-Einstellung mit sofortiger Wirkung | Switch |
Checkbox (wirkt wie Bestätigen/Absenden) |
| Unabhängige Mehrfachauswahl, Zustimmung | Checkbox |
Switch |
| Eine von 2–5 Optionen, alle sichtbar | Radio oder SegmentedButton |
Dropdown (versteckt die Optionen) |
| Eine von vielen Optionen | DropdownButton | Lange Radio-Liste |
| Ungefährer Zahlenwert | Slider |
TextField |
| Exakter Zahlenwert (Alter, Menge) | TextField mit Zifferntastatur |
Slider (exakte Werte trifft man schwer) |
Noch eine Unterscheidung: Speichern diese Widgets direkt bei der Änderung, genügen sie für sich allein. Werden die Eingaben dagegen gemeinsam mit einem „Absenden“-Button geprüft – wie das Kästchen „Ich akzeptiere die Bedingungen“ bei einer Registrierung –, müssen sie über ein FormField in das Form-Widget eingebunden werden.
Häufige Fehler
1. setState vergessen
Symptom: Sie tippen auf den Switch, onChanged läuft, aber optisch ändert sich nichts. Da diese Widgets ihren Wert nicht selbst halten, bleibt der Bildschirm beim alten value, solange die Variable nicht innerhalb von setState aktualisiert wird.
// Falsch: Die Variable ändert sich, aber der Bildschirm wird nicht neu gezeichnet
onChanged: (value) => isSwitched = value,
// Richtig
onChanged: (value) => setState(() => isSwitched = value),2. onChanged unbemerkt null übergeben
Symptom: Das Widget ist grau und reagiert nicht auf Berührung. onChanged: null bedeutet in Flutter „deaktiviert“. Bewusst eingesetzt ist das sehr nützlich (zum Beispiel, um Einstellungen während des Speicherns zu sperren), dasselbe Ergebnis entsteht aber versehentlich, wenn ein eigenes Widget einen optionalen Callback-Parameter hat und Sie vergessen, ihn zu übergeben.
3. Slider-Wert außerhalb des Bereichs
Symptom: ein Assertion-Fehler mit value >= min && value <= max. Er tritt meist auf, wenn der Wert aus einer gespeicherten Einstellung stammt oder max dynamisch kleiner wird. Begrenzen Sie den Wert, bevor Sie ihn dem Slider übergeben:
Slider(
value: savedValue.clamp(0, 100).toDouble(),
min: 0,
max: 100,
onChanged: (v) => setState(() => savedValue = v),
)4. Bei jedem Slider-Schritt speichern
onChanged feuert immer wieder, solange sich der Finger bewegt. Steht dort ein Schreibzugriff oder ein Netzwerkaufruf, ruckelt das Ziehen, und Dutzende unnötiger Anfragen gehen raus. In onChanged gehört nur setState; die dauerhafte Arbeit erledigen Sie in onChangeEnd, das beim Loslassen ausgeführt wird.
5. LinearProgressIndicator ungeschützt in eine Row setzen
Symptom: der Fehler „BoxConstraints forces an infinite width“. Eine lineare Anzeige möchte die gesamte verfügbare Breite, eine Row bietet ihren Kindern aber unbegrenzte Breite an. Packen Sie die Anzeige in ein Expanded oder geben Sie ihr mit SizedBox eine Breite.
Mini-Szenario: ein Einstellungsbildschirm
Sehen wir uns alles zusammen an: ein Schalter für Benachrichtigungen, eine Theme-Auswahl, ein Slider für die Schriftgröße und ein dünner Fortschrittsbalken, der beim Speichern oben erscheint.
enum AppTheme { system, light, dark }
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool _notifications = true;
AppTheme _theme = AppTheme.system;
double _fontScale = 1.0;
bool _isSaving = false;
Future<void> _save() async {
setState(() => _isSaving = true);
await Future.delayed(const Duration(milliseconds: 600)); // Platzhalter für das Speichern
if (!mounted) return;
setState(() => _isSaving = false);
}
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Behält seinen Platz, damit der Inhalt nicht springt
Opacity(
opacity: _isSaving ? 1 : 0,
child: const LinearProgressIndicator(),
),
SwitchListTile(
title: const Text('Benachrichtigungen'),
value: _notifications,
onChanged: (value) {
setState(() => _notifications = value);
_save();
},
),
const SizedBox(height: 8),
SegmentedButton<AppTheme>(
segments: const [
ButtonSegment(value: AppTheme.system, label: Text('System')),
ButtonSegment(value: AppTheme.light, label: Text('Hell')),
ButtonSegment(value: AppTheme.dark, label: Text('Dunkel')),
],
selected: {_theme},
onSelectionChanged: (selection) {
setState(() => _theme = selection.first);
_save();
},
),
const SizedBox(height: 16),
Text('Schriftgröße: ${(_fontScale * 100).round()} %'),
Slider(
value: _fontScale,
min: 0.8,
max: 1.4,
divisions: 6,
label: '${(_fontScale * 100).round()} %',
onChanged: (value) => setState(() => _fontScale = value),
onChangeEnd: (_) => _save(),
),
],
);
}
}Switch und Theme-Auswahl speichern sofort bei der Änderung; der Slider aktualisiert beim Ziehen nur den Bildschirm und speichert erst beim Loslassen. Den Fortschrittsbalken blenden wir nicht per if ein und aus, sondern verstecken ihn mit Opacity – so rutschen die Zeilen darunter nicht hoch und runter. Weil diese Einstellungen mehr als einen Bildschirm betreffen, gehören die Werte in einem echten Projekt nicht in den State dieser Seite, sondern in eine State-Management-Lösung wie Provider. Was an die Stelle der Zeile Future.delayed in _save tritt, also das echte dauerhafte Speichern, damit die Werte ein Schließen der App überstehen, zeigt der Beitrag lokale Datenspeicherung mit SharedPreferences.
Häufig gestellte Fragen
Was ist der Unterschied zwischen Switch und Checkbox?
Ein Switch schaltet eine Einstellung mit sofortiger Wirkung ein oder aus, wie ein Lichtschalter. Eine Checkbox dient unabhängigen Auswahlen aus einer Liste oder einer Zustimmung vor dem Absenden, und ihre Wirkung tritt meist erst mit „Speichern“ oder „Absenden“ ein.
Soll ich Radio oder DropdownButton verwenden?
Bei wenigen Optionen (bis etwa fünf), die man auf einen Blick sehen sollte, ist Radio oder SegmentedButton besser. Bei vielen Optionen oder wenig Platz wählen Sie DropdownButton.
Warum wird das Label des Sliders nicht angezeigt?
Im Standard-Theme erscheint die Werteblase nur bei Slidern mit dem Parameter divisions, also bei gestuften Slidern. divisions hinzuzufügen oder showValueIndicator über ein SliderTheme zu ändern, löst das Problem.
CircularProgressIndicator oder LinearProgressIndicator?
Lässt sich der Fortschritt nicht messen und wartet der ganze Bildschirm, passt eine kreisförmige Anzeige in der Mitte. Ist der Fortschritt als Prozentwert bekannt oder bleibt der Rest des Bildschirms nutzbar, stört eine dünne lineare Anzeige am oberen Rand weniger.
Verwandte Artikel
Flutter: Helles und dunkles Theme mit ThemeData
Ein Flutter-Theme mit ThemeData aufbauen: ColorScheme.fromSeed, helles und dunkles Theme, ThemeMode, Theme.of, TextTheme, Komponenten-Themes, ThemeExtension.
Flutter: TextField und TextEditingController verwenden
TextField in Flutter: TextEditingController und dispose, InputDecoration, Tastaturtypen, inputFormatters, Passwortfelder, FocusNode und onChanged im Vergleich.
Flutter: Widgets überlagern mit Stack und Positioned
Stack und Positioned in Flutter: wie ein Stack seine Größe bestimmt, fit, alignment, clipBehavior, Positioned.fill, PositionedDirectional, Badge und Overlays.