İçeriğe geç / Skip to content / Zum Inhalt

Flutter: Eingabe-Widgets und Statusanzeigen

Ahmet Balaman

Zuletzt aktualisiert:

7 Min. Lesezeit

FlutterSwitchCheckboxRadioSliderProgressIndicator
Flutter: Eingabe-Widgets und Statusanzeigen

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.

Kommentare