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

Flutter: Eigene Widgets erstellen

Ahmet Balaman

Zuletzt aktualisiert:

7 Min. Lesezeit

FlutterWidgetStatelessWidgetStatefulWidgetConstructorconst
Flutter: Eigene Widgets erstellen

In Flutter bauen wir Bildschirme, indem wir fertige Widgets ineinander verschachteln – und irgendwann ertappen wir uns dabei, dieselbe Struktur aus Card + Padding + Column zum dritten Mal zu tippen. Genau hier setzt ein eigenes Widget an: Sie geben dem wiederkehrenden UI-Baustein einen Namen und machen die veränderlichen Teile zu Konstruktorparametern. Das Ergebnis sind kürzere build-Methoden, Änderungen an einer einzigen Stelle und, richtig eingesetzt, weniger Rebuilds.

Live-Demo: eigene Widgets erstellen

Lernen Sie interaktiv, wie Sie eigene Widgets erstellen:

💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.

Grundstruktur mit StatelessWidget

Für Widgets, die keine eigenen veränderlichen Daten halten und nur die erhaltenen Parameter anzeigen, verwenden wir StatelessWidget:

class MyCustomWidget extends StatelessWidget {
  const MyCustomWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const Padding(
      padding: EdgeInsets.all(16),
      child: Text('Mein eigenes Widget'),
    );
  }
}

Zwei Details wiederholen sich in jedem eigenen Widget. super.key ist die Kurzform, um den Parameter key an die Oberklasse weiterzureichen; sie leistet dasselbe wie die Schreibweise {Key? key}) : super(key: key), die Ihnen in älteren Beispielen begegnet. Das const vor dem Konstruktor erlaubt es, das Widget als const MyCustomWidget() zu erzeugen. Flutter weiß, dass sich ein const-Widget nicht geändert hat, und kann es überspringen, wenn das Eltern-Widget neu aufgebaut wird.

Parameter über den Konstruktor

Parameter machen ein Widget anpassbar. Felder sind immer final; die Werte kommen über den Konstruktor:

class CustomButton extends StatelessWidget {
  const CustomButton({
    super.key,
    required this.text,
    required this.onPressed,
    this.icon,
  });

  final String text;
  final VoidCallback onPressed;
  final IconData? icon;

  @override
  Widget build(BuildContext context) {
    return FilledButton(
      onPressed: onPressed,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (icon != null) ...[
            Icon(icon, size: 18),
            const SizedBox(width: 8),
          ],
          Text(text),
        ],
      ),
    );
  }
}

Verwendung:

CustomButton(
  text: 'Speichern',
  icon: Icons.save,
  onPressed: () {
    print('Gespeichert');
  },
)

Das Widget weiß nicht, was der Nutzer gerade tut; es meldet sich nur über onPressed nach außen. Die Entscheidung trifft der Bildschirm darüber. Dieser Fluss – „Daten nach unten, Ereignisse nach oben“ – ist die Grundregel beim Entwurf eigener Widgets.

Pflichtparameter, optionale Parameter und Standardwerte

class CustomCard extends StatelessWidget {
  const CustomCard({
    super.key,
    required this.title,         // Pflicht
    this.subtitle,               // Optional (nullable)
    this.padding = 16,           // Mit Standardwert
  });

  final String title;
  final String? subtitle;
  final double padding;

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;

    return Card(
      child: Padding(
        padding: EdgeInsets.all(padding),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(title, style: textTheme.titleMedium),
            if (subtitle != null) // Anzeigen, falls vorhanden
              Text(subtitle!, style: textTheme.bodySmall),
          ],
        ),
      ),
    );
  }
}
const CustomCard(title: 'Titel') // ohne Untertitel

const CustomCard(
  title: 'Titel',
  subtitle: 'Untertitel',
  padding: 24,
)

Die Auswahlregel ist einfach: required, wenn das Widget ohne den Wert keinen Sinn ergibt; nullable, wenn es auch ohne ihn funktioniert; ein Standardwert, wenn die meisten Aufrufer ohnehin dasselbe übergeben würden. Details zur Karte selbst stehen im Card-Beitrag.

Flexible Widgets mit einem child-Parameter

Statt für jede Möglichkeit einen Parameter hinzuzufügen, ist es oft das bessere Design, den Inhalt von außen entgegenzunehmen. Flutters eigene Widgets arbeiten genauso:

class SectionBox extends StatelessWidget {
  const SectionBox({
    super.key,
    required this.child,
    this.borderRadius = 12,
  });

  final Widget child;
  final double borderRadius;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;

    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: colors.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(borderRadius),
      ),
      child: child,
    );
  }
}
const SectionBox(child: Text('Inhalt'))

SectionBox(
  child: Column(
    children: const [Text('Titel'), Text('Untertitel')],
  ),
)

Widget-Klasse oder Hilfsmethode?

Eine lange build-Methode lässt sich auf zwei Arten zerlegen: mit privaten Methoden wie _buildHeader(), die ein Widget zurückgeben, oder indem Sie den Baustein in eine eigene Widget-Klasse auslagern. Ersteres braucht weniger Code, doch beides ist nicht dasselbe:

// Hilfsmethode: läuft bei jedem Build des Eltern-Widgets erneut
Widget _buildHeader() {
  return Row(
    children: [
      CircleAvatar(child: Text(author[0])),
      const SizedBox(width: 8),
      Text(author),
    ],
  );
}

// Eigenes Widget: kann const sein, hat einen eigenen BuildContext
class _PostHeader extends StatelessWidget {
  const _PostHeader({required this.author});

  final String author;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        CircleAvatar(child: Text(author[0])),
        const SizedBox(width: 8),
        Text(author),
      ],
    );
  }
}

Für Flutter ist eine Hilfsmethode kein eigenes Widget; ihr Ergebnis wird direkt in das build-Ergebnis des Eltern-Widgets eingefügt. Jedes setState im Eltern-Widget lässt auch die Methode erneut laufen. Eine eigene Klasse kann dagegen als const erzeugt werden und bleibt vom Neuaufbau verschont, solange sich ihre Parameter nicht ändern – auf häufig aktualisierten Bildschirmen macht das einen Unterschied. Außerdem besitzt sie einen eigenen BuildContext, erscheint unter ihrem Namen im Widget-Baum und lässt sich einzeln testen.

Eine praktische Regel: Ist der Baustein nur wenige Zeilen lang und nur in diesem einen build sinnvoll, darf es eine Methode bleiben. Wächst er, wird er woanders gebraucht oder baut sich das Eltern-Widget oft neu auf, lagern Sie eine Klasse aus. Der führende Unterstrich (_PostHeader) macht die Klasse dateiprivat; nicht jeder Baustein muss dem ganzen Projekt offenstehen. Wie der Baum aufgebaut ist, können Sie im Beitrag zum Widget-Baum auffrischen.

Eigenes Widget mit StatefulWidget

Muss ein Widget einen Wert halten, der sich in seinem Inneren ändert, verwenden Sie StatefulWidget. Eine Mengenauswahl ist ein gutes Beispiel:

Ein StatelessWidget nimmt einen Parameter und zeichnet ihn, ein StatefulWidget hält eigenen State und startet build über setState neu

class QuantitySelector extends StatefulWidget {
  const QuantitySelector({
    super.key,
    this.initialValue = 1,
    required this.onChanged,
  });

  final int initialValue;
  final ValueChanged<int> onChanged;

  @override
  State<QuantitySelector> createState() => _QuantitySelectorState();
}

class _QuantitySelectorState extends State<QuantitySelector> {
  late int _quantity = widget.initialValue;

  void _update(int value) {
    if (value < 1) return;
    setState(() => _quantity = value);
    widget.onChanged(value);
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        IconButton(
          icon: const Icon(Icons.remove),
          onPressed: () => _update(_quantity - 1),
        ),
        Text('$_quantity', style: Theme.of(context).textTheme.titleLarge),
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: () => _update(_quantity + 1),
        ),
      ],
    );
  }
}

Das Widget hält die Zahl selbst, meldet aber jede Änderung über onChanged nach außen; die Warenkorbsumme zu berechnen ist Sache des übergeordneten Bildschirms. initState, dispose und die übrigen Schritte behandle ich ausführlich im Beitrag zum Lebenszyklus.

Wann verwenden – und wann nicht?

Sehen Sie eines dieser Anzeichen, ist es Zeit, einen Baustein in ein eigenes Widget auszulagern:

  • Dieselbe Struktur wird zum zweiten oder dritten Mal geschrieben.
  • Die build-Methode passt nicht mehr auf einen Bildschirm, die Einrückung ist unlesbar geworden.
  • Ein kleiner Teil des Bildschirms aktualisiert sich häufig, und setState baut die ganze Seite neu auf.

In diesen Fällen ist ein eigenes Widget überflüssig oder das falsche Werkzeug:

  • Es ändert sich nur der Stil: Statt einen Wrapper MyButton zu schreiben, damit alle Buttons dieselbe Farbe haben, konfigurieren Sie die Button-Themes in ThemeData. Ein Theme wird an einer Stelle gepflegt und wirkt auf alle eingebauten Widgets.
  • Ein einmaliges Fragment aus drei Zeilen: Wer jedes Paar aus Padding + Text in eine Klasse verwandelt, verstreut Code über Dateien und erschwert das Lesen.
  • Die Parameterzahl läuft aus dem Ruder: Ein Widget mit zehn bool-Flags möchte eigentlich zwei oder drei getrennte Widgets sein. Parameter, die Widgets entgegennehmen – etwa child oder leading/trailing –, sind flexibler als Flags.

Häufige Fehler

1. Nicht-finales Feld in einem StatelessWidget

Symptom: Der Analyzer warnt „This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final“; selbst wenn Sie das Feld ändern, aktualisiert sich der Bildschirm nicht.

// Falsch
class PriceTag extends StatelessWidget {
  PriceTag({super.key, required this.price});
  double price; // nicht final
  // ...
}

Widgets sind unveränderliche Objekte. Ändert sich der Wert von außen, baut das Eltern-Widget mit dem neuen Wert neu auf; ändert er sich im Widget selbst, brauchen Sie ein StatefulWidget mit einem Feld in dessen State.

2. Keinen const-Konstruktor schreiben

Symptom: const MyWidget() führt zu „The constructor being called isn't a const constructor“. Damit ein Konstruktor const sein kann, müssen lediglich alle Felder final sein. Der Code läuft auch ohne const, aber Sie verschenken die Chance, dass Flutter das Widget bei Rebuilds überspringt. Wer den Analyzer-Hinweisen prefer_const_constructors folgt, gewöhnt sich das von selbst an.

3. Den Callback aufrufen, statt ihn zu übergeben

// Falsch: Die Funktion läuft sofort während des Builds
CustomButton(text: 'Löschen', onPressed: _delete())

// Richtig: Die Funktion selbst wird übergeben
CustomButton(text: 'Löschen', onPressed: _delete)

Symptom: Die Aktion läuft, sobald der Bildschirm erscheint – noch bevor der Button gedrückt wurde. Enthält sie ein setState, erscheint „setState() or markNeedsBuild() called during build“. Klammern rufen die Funktion auf; schreiben Sie sie ohne Klammern oder verwenden Sie () => _delete().

4. Einen Parameter in den State kopieren und die Aktualisierung vergessen

Symptom: Das Eltern-Widget schickt einen neuen Wert, auf dem Bildschirm bleibt aber der alte stehen. Da initState nur einmal läuft, bekommt ein dort kopierter Wert spätere Änderungen nie mit. Wird der Wert nur angezeigt, lesen Sie widget.value direkt, ohne zu kopieren; muss er wirklich kopiert werden, aktualisieren Sie ihn in didUpdateWidget:

@override
void didUpdateWidget(covariant QuantitySelector oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.initialValue != widget.initialValue) {
    _quantity = widget.initialValue;
  }
}

Mini-Szenario: ein Widget für den leeren Zustand

Fast jede App braucht die Ansicht „Hier ist noch nichts“ an mehreren Stellen: leerer Warenkorb, leere Favoriten, Suche ohne Treffer. Alle teilen dasselbe Gerüst: Icon, Titel, Beschreibung und ein optionaler Button.

class EmptyState extends StatelessWidget {
  const EmptyState({
    super.key,
    required this.icon,
    required this.title,
    required this.message,
    this.actionLabel,
    this.onAction,
  });

  final IconData icon;
  final String title;
  final String message;
  final String? actionLabel;
  final VoidCallback? onAction;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(icon, size: 64, color: theme.colorScheme.outline),
            const SizedBox(height: 16),
            Text(title, style: theme.textTheme.titleLarge),
            const SizedBox(height: 8),
            Text(
              message,
              textAlign: TextAlign.center,
              style: theme.textTheme.bodyMedium,
            ),
            if (actionLabel != null && onAction != null) ...[
              const SizedBox(height: 24),
              FilledButton(onPressed: onAction, child: Text(actionLabel!)),
            ],
          ],
        ),
      ),
    );
  }
}

Im Warenkorb-Bildschirm reduziert sich die Verwendung auf eine einzige Bedingung:

body: items.isEmpty
    ? EmptyState(
        icon: Icons.shopping_cart_outlined,
        title: 'Ihr Warenkorb ist leer',
        message: 'Produkte, die Sie in den Warenkorb legen, erscheinen hier.',
        actionLabel: 'Jetzt einkaufen',
        onAction: () => Navigator.pop(context),
      )
    : CartList(items: items),

Weil Farben und Textstile aus dem Theme stammen, statt fest verdrahtet zu sein, sieht das Widget im hellen wie im dunklen Theme ohne Zusatzarbeit richtig aus. Der Button erscheint, wenn beide Parameter gesetzt sind; für einen rein informativen leeren Zustand lassen Sie einfach beide weg.

Häufig gestellte Fragen

Soll ich StatelessWidget oder StatefulWidget verwenden?

Zeigt das Widget nur Werte an, die es von außen erhält, genügt StatelessWidget. Hält es einen Wert, der sich mit der Zeit ändert, einen Controller oder eine Animation, braucht es StatefulWidget. Im Zweifel beginnen Sie mit StatelessWidget und stellen um, sobald der Bedarf entsteht.

Was ist super.key, und muss ich es schreiben?

super.key ist die Kurzschreibweise, die den key-Parameter des Widgets an die Oberklasse weiterreicht. Pflicht ist es nicht, aber ein Key wird gebraucht, wenn Widgets in Listen umsortiert werden oder ihr Zustand erhalten bleiben soll – deshalb gehört es standardmäßig in jedes eigene Widget.

Was bewirkt ein const-Konstruktor?

Ein mit const erzeugtes Widget entsteht zur Kompilierzeit als einzelne Instanz, und Flutter weiß, dass es sich nicht geändert hat. Baut sich das Eltern-Widget neu auf, kann dieser Teilbaum übersprungen werden, was vor allem auf häufig aktualisierten Bildschirmen unnötige Arbeit spart.

Hilfsmethode oder eigene Widget-Klasse – was ist besser?

Für kleine Bausteine, die nur innerhalb eines einzigen build sinnvoll sind, reicht eine Hilfsmethode. Wächst der Baustein, wird er wiederverwendet oder baut sich das Eltern-Widget oft neu auf, ist eine eigene Widget-Klasse die bessere Wahl, weil sie const sein kann und einen eigenen BuildContext besitzt.

Kommentare