Dart Enum und Composition: Code sauber organisieren
Zuletzt aktualisiert:
7 Min. Lesezeit

Im Code von Dart-Einsteigern tauchen zwei Gewohnheiten immer wieder auf. Die erste: feste Auswahlmöglichkeiten als Text darstellen, sodass eine Pizzagröße als die Strings "klein", "mittel" und "gross" durch das Programm wandert. Die zweite: eine Beziehung zwischen zwei Klassen durch Kopieren von Feldern herstellen, sodass Name, Alter und ID des Besitzers einzeln in die Auto-Klasse geschrieben werden. Dieser Beitrag behandelt Darts Antwort auf beide Probleme: Enums und Composition.
Wenn Sie Typumwandlungen und statische Member gelesen haben, ist die Grundlage gelegt. Die Beispiele sind für Dart 3 geschrieben; Switch-Ausdrücke und Enums mit Feldern, die Sie in älteren Tutorials nicht finden, stehen hier im Mittelpunkt.
Live-Demo: Dart Enum und Composition
Probieren Sie den Einsatz von Enum und Composition interaktiv aus:
Das Problem mit Optionen als Strings
Sehen wir uns zuerst das Problem an, das Enums lösen:
void bestellen(String groesse) {
if (groesse == 'mittel') {
print('Mittlere Pizza wird zubereitet');
} else if (groesse == 'gross') {
print('Große Pizza wird zubereitet');
} else {
print('Kleine Pizza wird zubereitet');
}
}
void main() {
bestellen('Mittel'); // Großbuchstabe: läuft still in den Zweig "klein"
}Der Code kompiliert, läuft und liefert ein falsches Ergebnis. Der Compiler kann den Unterschied zwischen 'Mittel' und 'mittel' nicht kennen, denn für ihn sind beides nur Strings. Ein Enum macht aus den möglichen Werten einen Typ; einen Wert, der nicht in der Liste steht, können Sie gar nicht erst hinschreiben.
Enum-Grundlagen
enum Farbe { rot, blau, weiss }
void main() {
final gewaehlt = Farbe.blau;
print(gewaehlt.name); // blau
print(gewaehlt.index); // 1
print(Farbe.values); // [Farbe.rot, Farbe.blau, Farbe.weiss]
}Jedes Enum bringt drei Dinge kostenlos mit: name liefert den Namen des Werts, index seine Position und values die Liste aller Werte. Außerdem schlägt Ihr Editor die Optionen vor, sobald Sie Farbe. tippen.
Switch und Vollständigkeitsprüfung
Mit Dart 3 hat sich beim Switch zweierlei geändert. Erstens verlässt die Ausführung den Switch automatisch, wenn ein nicht leerer case endet; die break-Zeilen aus älteren Beispielen sind nicht mehr nötig. Zweitens muss ein Switch über ein Enum vollständig sein: Lassen Sie einen Wert aus, kompiliert der Code nicht.
Wenn ein Wert erzeugt werden soll, ist ein Switch-Ausdruck kürzer:
enum Farbe { rot, blau, weiss }
String farbName(Farbe farbe) => switch (farbe) {
Farbe.rot => 'Rot',
Farbe.blau => 'Blau',
Farbe.weiss => 'Weiß',
};
void main() {
print(farbName(Farbe.blau)); // Blau
}Stellen Sie sich vor, Sie ergänzen eines Tages gruen. Der Compiler meldet jeden Switch, der den neuen Wert nicht behandelt, als Fehler; Sie müssen die anzupassenden Stellen nicht suchen.
Hier passiert Lernenden oft ein Fehler: Sie hängen „sicherheitshalber“ ein default oder _ an den Switch. Damit ist die Vollständigkeitsprüfung abgeschaltet. Der neue Wert gruen landet still im Standardzweig, und den Fehler bemerken Sie erst zur Laufzeit. Wer bei einem Switch über ein Enum alle Werte ausdrücklich aufführt, lässt den Compiler für sich Wache halten.
Enums mit Feldern und Methoden
Enums sind mehr als eine Liste von Namen. Seit Dart 2.17 können Sie jedem Wert Daten zuordnen und dem Enum Methoden und Getter geben:
import 'dart:math';
enum PizzaGroesse {
klein(durchmesser: 25, preis: 180),
mittel(durchmesser: 30, preis: 240),
gross(durchmesser: 35, preis: 310);
const PizzaGroesse({required this.durchmesser, required this.preis});
final int durchmesser; // Zentimeter
final int preis;
double get flaeche => pi * (durchmesser / 2) * (durchmesser / 2);
double get preisProQuadratzentimeter => preis / flaeche;
bool istGroesserAls(PizzaGroesse andere) => durchmesser > andere.durchmesser;
}
void main() {
const auswahl = PizzaGroesse.mittel;
print('${auswahl.name}: ${auswahl.durchmesser} cm, ${auswahl.preis}');
print(auswahl.preisProQuadratzentimeter.toStringAsFixed(3));
print(PizzaGroesse.gross.istGroesserAls(auswahl)); // true
}Jetzt steht alles, was zu einer Größe gehört, an einer Stelle. Verstreute Zuordnungen wie if (groesse == 'mittel') preis = 240; in irgendeiner anderen Datei fallen weg.
Die Dokumentation nennt die Regeln für solche Enums klar:
- Alle Werte werden ganz am Anfang des Enums deklariert, und es muss mindestens einen geben.
- Instanzvariablen müssen
finalsein, auch solche aus Mixins. - Generative Konstruktoren müssen
constsein. Ein Factory-Konstruktor darf nur einen der vorhandenen Werte zurückgeben. - Ein Enum kann keine andere Klasse per
extendserweitern, da es automatischEnumerweitert.implementsundwithsind aber erlaubt. index,hashCodeund==lassen sich nicht überschreiben, und kein Member darfvaluesheißen.
Von Text zu Enum: byName und asNameMap
Das Paar int.parse / int.tryParse aus dem vorigen Beitrag hat ein Gegenstück für Enums. So wird der Text 'mittel' aus JSON oder lokalem Speicher wie shared_preferences zum Enum-Wert:
enum PizzaGroesse { klein, mittel, gross }
void main() {
final groesse = PizzaGroesse.values.byName('mittel'); // PizzaGroesse.mittel
print(groesse);
// PizzaGroesse.values.byName('riesig'); // wirft einen ArgumentError
final sicher = PizzaGroesse.values.asNameMap()['riesig'] ?? PizzaGroesse.mittel;
print(sicher); // PizzaGroesse.mittel
}Speichern Sie außerdem name statt index. An dem Tag, an dem Sie die Reihenfolge ändern oder einen Wert dazwischen einfügen, zeigt eine gespeicherte 1 plötzlich auf einen anderen Wert. Der Text 'mittel' bleibt dagegen gleich, solange Sie den Wert nicht umbenennen.
Bildschirmzustand mit einem Enum in Flutter
Dass eine Seite lädt, erfolgreich ist, fehlschlägt oder leer ist, ist ein klassisches Enum-Beispiel. Ein Switch-Ausdruck kann direkt ein Widget zurückgeben:
import 'package:flutter/material.dart';
enum LadeZustand { laedt, erfolg, fehler, leer }
Widget zeigeFuerZustand(LadeZustand zustand) => switch (zustand) {
LadeZustand.laedt => const CircularProgressIndicator(),
LadeZustand.erfolg => const Text('Daten geladen'),
LadeZustand.fehler => const Text('Ein Fehler ist aufgetreten'),
LadeZustand.leer => const Text('Keine Inhalte gefunden'),
};Hier zeigt sich auch die Grenze eines Enums: Jeder Wert ist eine einzelne Konstante, Sie können dem Zustand „Fehler“ also nicht die aktuelle Fehlermeldung und dem Zustand „Erfolg“ nicht die geladene Liste mitgeben. Müssen Zustände eigene Daten tragen, kommen die sealed-Klassen aus Dart 3 ins Spiel; mehr dazu im Beitrag über Vererbung und Polymorphie.
Composition: die „Hat ein“-Beziehung
Composition bedeutet, dass eine Klasse ein Objekt einer anderen Klasse als Feld hält. Die Beziehung liest sich als „ein Auto hat einen Besitzer“, daher der Name Hat-ein-Beziehung (has-a).
class Person {
final int id;
final String name;
final int alter;
const Person({required this.id, required this.name, required this.alter});
}
class Auto {
final Person besitzer; // Composition: ein Auto hat eine Person
final String modell;
final String kennzeichen;
const Auto({required this.besitzer, required this.modell, required this.kennzeichen});
}
void main() {
const ahmet = Person(id: 1233, name: 'Ahmet', alter: 25);
const auto = Auto(besitzer: ahmet, modell: 'Toyota Corolla', kennzeichen: '34 ABC 123');
print('Auto von ${auto.besitzer.name}: ${auto.modell}');
print('Kennzeichen: ${auto.kennzeichen}');
}Die Personendaten stehen in einer einzigen Klasse. Morgen können Klassen wie Haus, Versicherung oder Termin dieselbe Person verwenden; wollen Sie eine Telefonnummer ergänzen, ändern Sie nur Person.
late und required gehören nicht zusammen
In einer früheren Fassung dieses Beitrags waren die Felder als late int id; deklariert und wurden im Konstruktor per required this.id gefüllt. Viele Beispiele im Netz verwenden dieses Muster noch, und Lernende übernehmen es ständig. Das Problem: late bedeutet „Ich weise dieses Feld später zu, irgendwo außerhalb des Konstruktors.“ required this.id garantiert aber bereits, dass das Feld beim Erzeugen des Objekts gesetzt wird. late bringt hier nichts, kostet aber zweierlei:
- Das Feld wird veränderbar; eine Zuweisung wie
person.id = 5kommt am Compiler vorbei. - Das Objekt kann nicht als
consterzeugt werden.
Richtig sind final-Felder und ein const-Konstruktor wie oben. Heben Sie late für Fälle auf, in denen Sie den Wert im Konstruktor wirklich nicht kennen (etwa einen Controller, der in initState entsteht).
Unveränderliche Objekte aktualisieren: copyWith
Sind die Felder final, kommt die Frage: „Wie aktualisiere ich das Alter?“ Die Antwort: Sie erzeugen ein neues Objekt, das die Änderung enthält:
class Person {
final int id;
final String name;
final int alter;
const Person({required this.id, required this.name, required this.alter});
Person copyWith({String? name, int? alter}) => Person(
id: id,
name: name ?? this.name,
alter: alter ?? this.alter,
);
}
void main() {
const person = Person(id: 1, name: 'Ahmet', alter: 25);
final einJahrSpaeter = person.copyWith(alter: 26);
print('${person.alter} -> ${einJahrSpaeter.alter}'); // 25 -> 26
}ThemeData.copyWith und TextStyle.copyWith in Flutter beruhen auf derselben Idee. Unveränderliche Objekte erleichtern State-Management-Werkzeugen außerdem die Frage „Haben sich die Daten geändert?“
Enums und Composition zusammen
In einem echten Modell stehen beide Konzepte nebeneinander. Eine Bestellung hat einen Kunden (Composition), eine Größe (Enum) und eine Liste von Extras:
enum PizzaGroesse {
klein(preis: 180),
mittel(preis: 240),
gross(preis: 310);
const PizzaGroesse({required this.preis});
final int preis;
}
enum Extra {
kaese(preis: 30),
pilze(preis: 25),
oliven(preis: 20);
const Extra({required this.preis});
final int preis;
}
class Person {
final String name;
const Person(this.name);
}
class Bestellung {
final Person kunde;
final PizzaGroesse groesse;
final List<Extra> extras;
const Bestellung({
required this.kunde,
required this.groesse,
this.extras = const [],
});
int get summe => extras.fold(groesse.preis, (s, e) => s + e.preis);
}
void main() {
const bestellung = Bestellung(
kunde: Person('Elif'),
groesse: PizzaGroesse.mittel,
extras: [Extra.kaese, Extra.pilze],
);
print('${bestellung.kunde.name}: ${bestellung.summe}'); // Elif: 295
}Preisdaten stehen in den Enums, Personendaten in Person, die Rechenlogik in Bestellung. Jede Klasse hat genau eine Verantwortung.
Flutter arbeitet ohnehin mit Composition
Wenn Sie in Flutter ein eigenes Widget schreiben, erweitern Sie nicht Card oder ListTile, sondern kombinieren sie in der build-Methode. Was wir Widget-Baum nennen, ist von oben bis unten Composition:
import 'package:flutter/material.dart';
class Benutzer {
final String name;
final String email;
const Benutzer({required this.name, required this.email});
}
class BenutzerKarte extends StatelessWidget {
const BenutzerKarte({super.key, required this.benutzer});
final Benutzer benutzer; // Composition: die Karte hat einen Benutzer
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: CircleAvatar(
child: Text(benutzer.name.isNotEmpty ? benutzer.name[0] : '?'),
),
title: Text(benutzer.name),
subtitle: Text(benutzer.email),
),
);
}
}Das einzige extends hier gilt StatelessWidget, dem Widget-Vertrag von Flutter. Die Ansicht selbst entsteht aus der Kombination kleiner Widgets.
Composition oder Vererbung?
Ein einfacher Test hilft bei der Entscheidung: Lesen Sie die Beziehung laut vor. Klingt „eine Katze ist ein Tier“ natürlich, passt Vererbung (is-a). Klingt „ein Auto hat einen Besitzer“ natürlich, passt Composition (has-a). Kommt ein seltsamer Satz wie „eine Bestellung ist eine Person“ heraus, wird Vererbung nur erzwungen, um Felder wiederzuverwenden. Im Zweifel bleibt Composition flexibler, denn ein enthaltenes Objekt auszutauschen ist viel einfacher, als eine Klassenhierarchie umzubauen.
Häufige Fehler
- Feste Optionen als Strings speichern; Tippfehler erreichen den Compiler nie.
- Einem Switch über ein Enum ein
defaultgeben; beim Hinzufügen eines Werts gibt es keine Warnung. - Ein Enum über seinen
indexspeichern; ändert sich die Reihenfolge, sind die gespeicherten Daten kaputt. - Felder als
latedeklarieren und mitrequired this.feldfüllen; verwenden Siefinal. - Zur Wiederverwendung von Code zur Vererbung greifen; lautet die Beziehung „hat ein“, nehmen Sie Composition.
Nächster Schritt
Für Zustandsdaten, die ein Enum nicht tragen kann, und für „ist ein“-Beziehungen folgt als Nächstes Vererbung und Polymorphie. Um Composition auch auf Verhaltensebene austauschbar zu machen, lesen Sie weiter bei Interfaces und implements.
Ressourcen
Kontakt:
Häufig gestellte Fragen
Können Enums in Dart Felder und Methoden haben?
Ja. Seit Dart 2.17 dürfen Enums final-Felder, einen const-Konstruktor, Getter und Methoden enthalten. So lässt sich etwa jeder Pizzagröße ein Preis zuordnen und über PizzaGroesse.mittel.preis auslesen.
Was passiert, wenn ich im switch einen Enum-Wert vergesse?
In Dart 3 muss ein switch über ein Enum vollständig sein; fehlt ein Wert, kompiliert der Code nicht. Ergänzen Sie das Enum später um einen Wert, zeigt Ihnen der Compiler jede Stelle, die angepasst werden muss. Mit einem default verzichten Sie auf diese Prüfung.
Soll ich einen Enum-Wert über name oder index speichern?
Verwenden Sie name. index hängt von der Reihenfolge der Werte ab, sodass alte Datensätze auf den falschen Wert zeigen, sobald Sie einen Wert ergänzen oder umsortieren. Beim Lesen nutzen Sie values.byName oder, um unbekannte Namen sicher abzufangen, values.asNameMap().
Composition oder Vererbung – was soll ich nehmen?
Liest sich die Beziehung als „ist ein“, passt Vererbung; liest sie sich als „hat ein“, passt Composition. Im Zweifel bleibt Composition meist die flexiblere Lösung.
Verwandte Artikel
Dart: Interface und implements richtig einsetzen
Verträge mit implements in Dart, interface class und abstract interface class aus Dart 3, der Unterschied zu Mixins mit with und Beispiel für testbaren Code.
Dart: Vererbung und Polymorphie – die Eltern-Kind-Beziehung
Vererbung in Dart mit extends, super und @override, Polymorphie, abstrakte Klassen, sichere Typprüfung und die Klassenmodifikatoren sealed, final und base.
Flutter-Kamera ohne erzwungene Ausrichtung: drei Fehler und ihre Lösungen
Gedrehte Vorschau, Einfrieren beim Aufnahmestart, tote Kamera: drei Flutter-Kamerafehler aus Recapday und der Code, der sie behoben hat.