Flutter/Dart Code-Review – Best Practices
Umfassende, bibliotheksunabhängige Checkliste für die Überprüfung von Flutter/Dart-Anwendungen. Diese Prinzipien gelten unabhängig davon, welche State-Management-Lösung, Routing-Bibliothek oder DI-Framework verwendet wird.
1. Allgemeine Projektgesundheit
- Projekt folgt einer konsistenten Ordnerstruktur (Feature-first oder Layer-first)
- Ordnungsgemäße Trennung der Belange: UI, Geschäftslogik, Datenebenen
- Keine Geschäftslogik in Widgets; Widgets sind rein präsentational
-
pubspec.yamlist sauber – keine ungenutzten Abhängigkeiten, Versionen angemessen fixiert -
analysis_options.yamlenthält ein strenges Lint-Set mit aktivierten strikten Analyzer-Einstellungen - Keine
print()-Anweisungen im Produktionscode – verwenden Siedart:developerlog()oder ein Logging-Paket - Generierte Dateien (
.g.dart,.freezed.dart,.gr.dart) sind aktuell oder in.gitignore - Plattformspezifischer Code ist hinter Abstraktionen isoliert
2. Dart-Sprachfallen
- Implizites dynamic: Fehlende Typannotationen führen zu
dynamic– aktivieren Siestrict-casts,strict-inference,strict-raw-types - Nullsicherheits-Missbrauch: Übermäßiger Einsatz des
!-Operators anstelle korrekter Null-Prüfungen oder Dart 3 Pattern Matching (if (value case var v?)) - Type-Promotion-Fehler: Verwendung von
this.field, obwohl lokale Variablen-Promotion funktionieren würde - Zu weites Fangen:
catch (e)ohneon-Klausel; geben Sie immer Ausnahmetypen an - Fangen von
Error:Error-Subtypen deuten auf Bugs hin und sollten nicht gefangen werden - Unbenutztes
async: Funktionen, die alsasyncmarkiert sind, aber nieawaitverwenden – unnötiger Overhead - Übermäßige Nutzung von
late:latewird verwendet, obwohl nullable oder Konstruktor-Initialisierung sicherer wäre; verschiebt Fehler in die Laufzeit - String-Konkatenation in Schleifen: Verwenden Sie
StringBufferanstelle von+für iterative Zeichenkettenbildung - Veränderlicher Zustand in
const-Kontexten: Felder inconst-Konstruktor-Klassen sollten nicht veränderbar sein - Ignorieren von
Future-Rückgabewerten: Verwenden Sieawaitoder rufen Sie explizitunawaited()auf, um die Absicht zu signalisieren -
varwofinalfunktioniert: Bevorzugen Siefinalfür lokale Variablen undconstfür Kompilierzeitkonstanten - Relative Imports: Verwenden Sie
package:-Imports für Konsistenz - Veränderliche Sammlungen offengelegt: Öffentliche APIs sollten unveränderliche Ansichten zurückgeben, nicht rohe
List/Map - Fehlendes Dart 3 Pattern Matching: Bevorzugen Sie Switch-Expressions und
if-casegegenüber ausführlichenis-Prüfungen und manuellem Casting - Wegwerfklassen für Mehrfachrückgaben: Verwenden Sie Dart 3 Records
(String, int)anstelle von einmaligen DTOs -
print()im Produktionscode: Verwenden Siedart:developerlog()oder das Logging-Paket des Projekts;print()hat keine Log-Level und kann nicht gefiltert werden
3. Widget-Best Practices
Widget-Zerlegung:
- Kein einzelnes Widget mit einer
build()-Methode, die ~80-100 Zeilen überschreitet - Widgets werden nach Kapselung UND nach Änderungsverhalten aufgeteilt (Rebuild-Grenzen)
- Private
_build*()-Hilfsmethoden, die Widgets zurückgeben, werden in separate Widget-Klassen extrahiert (ermöglicht Elementwiederverwendung, const-Weitergabe und Framework-Optimierungen) - Stateless Widgets werden Stateful vorgezogen, wenn kein veränderlicher lokaler Zustand benötigt wird
- Extrahierte Widgets befinden sich in separaten Dateien, wenn sie wiederverwendbar sind
Const-Nutzung:
-
const-Konstruktoren werden wo immer möglich verwendet – verhindert unnötige Rebuilds -
const-Literale für Sammlungen, die sich nicht ändern (const [],const {}) - Konstruktor wird als
constdeklariert, wenn alle Felder final sind
Key-Nutzung:
-
ValueKeywird in Listen/Gittern verwendet, um den Zustand bei Neuanordnungen zu erhalten -
GlobalKeywird sparsam verwendet – nur wenn der Zugriff auf den Zustand über den gesamten Baum hinweg wirklich benötigt wird -
UniqueKeywird inbuild()vermieden – erzwingt einen Rebuild bei jedem Frame -
ObjectKeywird verwendet, wenn die Identität auf einem Datenobjekt und nicht auf einem einzelnen Wert basiert
Theming & Designsystem:
- Farben stammen aus
Theme.of(context).colorScheme– keine hartcodiertenColors.red- oder Hex-Werte - Textstile stammen aus
Theme.of(context).textTheme– kein Inline-TextStylemit rohen Schriftgrößen - Dunkelmodus-Kompatibilität überprüft – keine Annahmen über hellen Hintergrund
- Abstände und Größen verwenden konsistente Design-Tokens oder Konstanten, keine magischen Zahlen
Build-Methoden-Komplexität:
- Keine Netzwerkaufrufe, Datei-I/O oder rechenintensive Operationen in
build() - Kein
Future.then()oderasync-Arbeit inbuild() - Keine Erstellung von Abonnements (
.listen()) inbuild() -
setState()auf den kleinstmöglichen Teilbaum beschränkt
4. State Management (Bibliotheksunabhängig)
Diese Prinzipien gelten für alle Flutter-State-Management-Lösungen (BLoC, Riverpod, Provider, GetX, MobX, Signals, ValueNotifier usw.).
Architektur:
- Geschäftslogik lebt außerhalb der Widget-Schicht – in einer State-Management-Komponente (BLoC, Notifier, Controller, Store, ViewModel usw.)
- State-Manager erhalten Abhängigkeiten per Injektion, nicht durch interne Konstruktion
- Eine Dienst- oder Repository-Schicht abstrahiert Datenquellen – Widgets und State-Manager sollten keine APIs oder Datenbanken direkt aufrufen
- State-Manager haben eine einzige Verantwortung – keine „God“-Manager, die nicht zusammenhängende Belange behandeln
- Abhängigkeiten zwischen Komponenten folgen den Konventionen der Lösung:
- In Riverpod: Provider, die von anderen Providern via
ref.watchabhängen, sind erwartet – kennzeichnen Sie nur zirkuläre oder übermäßig verworrene Ketten - In BLoC: Blocs sollten nicht direkt von anderen Blocs abhängen – bevorzugen Sie gemeinsame Repositories oder Koordination in der Präsentationsschicht
- In anderen Lösungen: Befolgen Sie die dokumentierten Konventionen für die Kommunikation zwischen Komponenten
- In Riverpod: Provider, die von anderen Providern via
Unveränderlichkeit & Wertegleichheit (für Lösungen mit unveränderlichem Zustand: BLoC, Riverpod, Redux):
- Zustandsobjekte sind unveränderlich – neue Instanzen werden über
copyWith()oder Konstruktoren erstellt, niemals direkt verändert - Zustandsklassen implementieren
==undhashCodekorrekt (alle Felder in den Vergleich einbezogen) - Mechanismus ist über das gesamte Projekt konsistent – manuelle Überschreibung,
Equatable,freezed, Dart Records oder andere - Sammlungen innerhalb von Zustandsobjekten werden nicht als rohe veränderliche
List/Mapoffengelegt
Reaktivitätsdisziplin (für reaktive Mutationslösungen: MobX, GetX, Signals):
- Zustand wird nur über die reaktive API der Lösung mutiert (
@actionin MobX,.valueauf Signalen,.obsin GetX) – direkte Feldmutation umgeht die Änderungsverfolgung - Abgeleitete Werte verwenden den berechneten Mechanismus der Lösung, anstatt redundant gespeichert zu werden
- Reaktionen und Disposer werden ordnungsgemäß bereinigt (
ReactionDisposerin MobX, Effektbereinigung in Signals)
Zustandsform-Design:
- Sich gegenseitig ausschließende Zustände verwenden versiegelte Typen, Union-Varianten oder den integrierten asynchronen Zustandstyp der Lösung (z. B. Riverpods
AsyncValue) – keine booleschen Flags (isLoading,isError,hasData) - Jeder asynchrone Vorgang modelliert Lade-, Erfolgs- und Fehlerzustände als separate Zustände
- Alle Zustandsvarianten werden in der UI erschöpfend behandelt – keine stillschweigend ignorierten Fälle
- Fehlerzustände tragen Fehlerinformationen zur Anzeige; Ladezustände enthalten keine veralteten Daten
- Nullable-Daten werden nicht als Ladeindikator verwendet – Zustände sind explizit
// SCHLECHT – Boolesche Flag-Suppe ermöglicht unmögliche Zustände
class UserState {
bool isLoading = false;
bool hasError = false; // isLoading && hasError ist darstellbar!
User? user;
}
// GUT (unveränderlicher Ansatz) – versiegelte Typen machen unmögliche Zustände undarstellbar
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
const UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
const UserError(this.message);
}
// GUT (reaktiver Ansatz) – beobachtbares Enum + Daten, Mutationen über die Reaktivitäts-API
// enum UserStatus { initial, loading, loaded, error }
// Verwenden Sie den observable/signal Ihrer Lösung, um Status und Daten getrennt zu wrappen
Rebuild-Optimierung:
- Zustandskonsumenten-Widgets (Builder, Consumer, Observer, Obx, Watch usw.) sind so eng wie möglich eingegrenzt
- Selektoren werden verwendet, um nur bei Änderungen bestimmter Felder neu zu bauen – nicht bei jeder Zustandsemission
-
const-Widgets werden verwendet, um die Rebuild-Weitergabe durch den Baum zu stoppen - Berechneter/abgeleiteter Zustand wird reaktiv berechnet, nicht redundant gespeichert
Abonnements & Freigabe:
- Alle manuellen Abonnements (
.listen()) werden indispose()/close()gekündigt - Stream-Controller werden geschlossen, wenn sie nicht mehr benötigt werden
- Timer werden im Freigabe-Lebenszyklus gekündigt
- Framework-verwalteter Lebenszyklus wird gegenüber manuellen Abonnements bevorzugt (deklarative Builder statt
.listen()) -
mounted-Prüfung vorsetStatein asynchronen Callbacks -
BuildContextwird nachawaitnicht ohne Überprüfung voncontext.mountedverwendet (Flutter 3.7+) – veralteter Context verursacht Abstürze - Keine Navigation, Dialoge oder Scaffold-Nachrichten nach asynchronen Lücken, ohne zu überprüfen, ob das Widget noch eingehängt ist
-
BuildContextwird niemals in Singletons, State-Managern oder statischen Feldern gespeichert
Lokaler vs. globaler Zustand:
- Flüchtiger UI-Zustand (Checkbox, Slider, Animation) verwendet lokalen Zustand (
setState,ValueNotifier) - Gemeinsam genutzter Zustand wird nur so hoch wie nötig gehoben – nicht übermäßig globalisiert
- Feature-begrenzter Zustand wird ordnungsgemäß freigegeben, wenn das Feature nicht mehr aktiv ist
5. Performance
Unnötige Rebuilds:
-
setState()wird nicht auf der obersten Widget-Ebene aufgerufen – lokalisieren Sie Zustandsänderungen -
const-Widgets werden verwendet, um die Rebuild-Weitergabe zu stoppen -
RepaintBoundarywird um komplexe Teilbäume verwendet, die unabhängig neu zeichnen - Der
child-Parameter vonAnimatedBuilderwird für Teilbäume verwendet, die unabhängig von der Animation sind
Teure Operationen in build():
- Kein Sortieren, Filtern oder Mapping großer Sammlungen in
build()– in der State-Management-Ebene berechnen - Keine Regex-Kompilierung in
build() -
MediaQuery.of(context)-Nutzung ist spezifisch (z. B.MediaQuery.sizeOf(context))
Bildoptimierung:
- Netzwerkbilder verwenden Caching (jede für das Projekt geeignete Caching-Lösung)
- Angemessene Bildauflösung für das Zielgerät (kein Laden von 4K-Bildern für Thumbnails)
-
Image.assetmitcacheWidth/cacheHeight, um in der Anzeigegröße zu dekodieren - Platzhalter- und Fehler-Widgets für Netzwerkbilder bereitgestellt
Lazy Loading:
-
ListView.builder/GridView.builderwird anstelle vonListView(children: [...])für große oder dynamische Listen verwendet (konkrete Konstruktoren sind für kleine, statische Listen in Ordnung) - Paginierung für große Datensätze implementiert
- Verzögertes Laden (
deferred as) für schwere Bibliotheken in Web-Builds
Sonstiges:
-
Opacity-Widget wird in Animationen vermieden – verwenden SieAnimatedOpacityoderFadeTransition - Clipping wird in Animationen vermieden – Bilder vorher zuschneiden
-
operator ==wird nicht auf Widgets überschrieben – stattdessenconst-Konstruktoren verwenden - Intrinsische Dimension-Widgets (
IntrinsicHeight,IntrinsicWidth) werden sparsam verwendet (zusätzlicher Layout-Durchlauf)
6. Testen
Testarten und Erwartungen:
- Unit-Tests: Deckt die gesamte Geschäftslogik ab (State-Manager, Repositories, Hilfsfunktionen)
- Widget-Tests: Deckt das Verhalten einzelner Widgets, Interaktionen und visuelle Ausgabe ab
- Integrationstests: Deckt kritische Benutzerabläufe End-to-End ab
- Golden-Tests: Pixelgenaue Vergleiche für designkritische UI-Komponenten
Abdeckungsziele:
- Ziel: 80 %+ Zeilenabdeckung der Geschäftslogik
- Alle Zustandsübergänge haben entsprechende Tests (Laden → Erfolg, Laden → Fehler, Wiederholung usw.)
- Randfälle getestet: leere Zustände, Fehlerzustände, Ladezustände, Grenzwerte
Testisolierung:
- Externe Abhängigkeiten (API-Clients, Datenbanken, Dienste) werden gemockt oder gefaked
- Jede Testdatei testet genau eine Klasse/Einheit
- Tests überprüfen das Verhalten, nicht die Implementierungsdetails
- Stubs definieren nur das für jeden Test benötigte Verhalten (minimales Stubbing)
- Kein gemeinsamer veränderlicher Zustand zwischen Testfällen
Widget-Testqualität:
-
pumpWidgetundpumpwerden korrekt für asynchrone Operationen verwendet -
find.byType,find.text,find.byKeywerden angemessen verwendet - Keine flaky Tests, die von Timing abhängen – verwenden Sie
pumpAndSettleoder explizitespump(Duration) - Tests laufen in CI und Fehler blockieren Merges
7. Barrierefreiheit
Semantische Widgets:
-
Semantics-Widget wird verwendet, um Screenreader-Labels bereitzustellen, wenn automatische Labels nicht ausreichen -
ExcludeSemanticswird für rein dekorative Elemente verwendet -
MergeSemanticswird verwendet, um verwandte Widgets zu einem einzigen zugänglichen Element zusammenzufassen - Bilder haben die Eigenschaft
semanticLabelgesetzt
Screenreader-Unterstützung:
- Alle interaktiven Elemente sind fokussierbar und haben sinnvolle Beschreibungen
- Die Fokus-Reihenfolge ist logisch (folgt der visuellen Lesereihenfolge)
Visuelle Barrierefreiheit:
- Kontrastverhältnis >= 4,5:1 für Text gegenüber Hintergrund
- Berührbare Ziele sind mindestens 48x48 Pixel groß
- Farbe ist nicht der einzige Indikator für den Status (Symbole/Text daneben verwenden)
- Text skaliert mit den Systemschriftgrößen-Einstellungen
Interaktions-Barrierefreiheit:
- Keine No-op-
onPressed-Callbacks – jeder Button tut etwas oder ist deaktiviert - Fehlerfelder schlagen Korrekturen vor
- Der Kontext ändert sich nicht unerwartet, während der Benutzer Daten eingibt
8. Plattformspezifische Belange
iOS/Android-Unterschiede:
- Plattformadaptive Widgets werden verwendet, wo angemessen
- Rückwärtsnavigation wird korrekt behandelt (Android-Zurück-Button, iOS-Wischen-zurück)
- Statusleiste und Safe-Area werden über das
SafeArea-Widget behandelt - Plattformspezifische Berechtigungen werden in
AndroidManifest.xmlundInfo.plistdeklariert
Responsive Design:
-
LayoutBuilderoderMediaQuerywerden für responsive Layouts verwendet - Breakpoints sind konsistent definiert (Telefon, Tablet, Desktop)
- Text läuft auf kleinen Bildschirmen nicht über – verwenden Sie
Flexible,Expanded,FittedBox - Querformat getestet oder explizit gesperrt
- Webspezifisch: Maus-/Tastaturinteraktionen unterstützt, Hover-Zustände vorhanden
9. Sicherheit
Sichere Speicherung:
- Sensitive Daten …