Dart Best Practices: Sauberer und wartbarer Code
# Dart Best Practices: Sauberen und wartbaren Code schreiben
Klare Konventionen machen Dart-Projekte langfristig stabil. Im Laufe der Jahre habe ich eine Reihe von Praktiken entwickelt, die Fehler drastisch reduzieren, das Onboarding beschleunigen und Code-Reviews deutlich vereinfachen. Dieser Leitfaden deckt alles ab, von Namenskonventionen bis zu den neuesten Dart-3+-Features.
Namenskonventionen
Gute Benennung macht die meisten Kommentare überflüssig. Der Dart Style Guide ist aus gutem Grund streng.
Klassen, Enums und Typedefs
Verwenden Sie `UpperCamelCase` für Typen. Namen sollten die Domäne widerspiegeln, nicht die Implementierung.
class=class="code-string">"code-comment">// Schlecht
class Mgr {
void doStuff() {}
}
class=class="code-string">"code-comment">// Gut
class AuthenticationManager {
void signInWithEmail(String email, String password) {}
}Variablen, Parameter und Funktionen
Verwenden Sie `lowerCamelCase`. Booleans sollten sich wie Behauptungen lesen.
class=class="code-string">"code-comment">// Schlecht
bool flag = true;
String s = class="code-string">'hallo';
int process(int x) => x * class="code-number">2;
class=class="code-string">"code-comment">// Gut
bool isAuthenticated = true;
String welcomeMessage = class="code-string">'hallo';
int doubleQuantity(int quantity) => quantity * class="code-number">2;Konstanten und Dateinamen
Verwenden Sie `lowerCamelCase` für Konstanten (nicht `SCREAMING_CAPS`). Dateinamen verwenden `snake_case`.
class=class="code-string">"code-comment">// Schlecht
const int MAX_RETRY_COUNT = class="code-number">3;
class=class="code-string">"code-comment">// Datei: AuthenticationManager.dart
class=class="code-string">"code-comment">// Gut
const int maxRetryCount = class="code-number">3;
class=class="code-string">"code-comment">// Datei: authentication_manager.dartPrivate Mitglieder
Beginnen Sie mit einem Unterstrich. Halten Sie die öffentliche API-Oberfläche so klein wie möglich.
class UserRepository {
final ApiClient _apiClient;
final Cache _cache;
UserRepository(this._apiClient, this._cache);
class=class="code-string">"code-comment">// Nur das exponieren, was Konsumenten tatsächlich brauchen
Future<User> fetchUser(String id) async {
return _cache.get(id) ?? await _fetchAndCache(id);
}
Future<User> _fetchAndCache(String id) async {
final user = await _apiClient.getUser(id);
_cache.set(id, user);
return user;
}
}Null Safety
Darts Sound Null Safety ist eine der größten Stärken der Sprache. Nutzen Sie sie bewusst, statt dagegen anzukämpfen.
Unnötige Nullable-Typen vermeiden
Jeder nullable Typ ist ein Fragezeichen, das Ihr zukünftiges Ich beantworten muss. Halten Sie die nicht-nullable Oberfläche so groß wie möglich.
class=class="code-string">"code-comment">// Schlecht - warum ist das nullable?
class UserProfile {
String? name;
String? email;
int? age;
UserProfile({this.name, this.email, this.age});
}
class=class="code-string">"code-comment">// Gut - Pflichtfelder sind nicht-nullable
class UserProfile {
final String name;
final String email;
final int age;
const UserProfile({
required this.name,
required this.email,
required this.age,
});
}`late` sparsam einsetzen
`late` ist ein Versprechen an den Compiler, dass Sie einen Wert vor dem Lesen initialisieren werden. Brechen Sie dieses Versprechen, erhalten Sie einen Laufzeitfehler, was den Zweck von Null Safety zunichte macht.
class=class="code-string">"code-comment">// Gefährlich - Laufzeitfehler bei Zugriff vor Initialisierung
late final DatabaseConnection _db;
class=class="code-string">"code-comment">// Sicherer - nullable mit expliziter Prüfung
DatabaseConnection? _db;
Future<DatabaseConnection> getDb() async {
return _db ??= await DatabaseConnection.open();
}Null-Aware Operatoren
Beherrschen Sie das vollständige Set: `??`, `??=`, `?.`, `?..` und `!`.
class=class="code-string">"code-comment">// Schlecht - umständliche Null-Prüfungen
String displayName;
if (user.nickname != null) {
displayName = user.nickname!;
} else {
displayName = user.fullName;
}
class=class="code-string">"code-comment">// Gut - prägnante Null-Aware-Kette
final displayName = user.nickname ?? user.fullName;
class=class="code-string">"code-comment">// Gut - Null-Aware Cascade
canvas?.drawRect(rect)
..fillColor = Colors.blue
..strokeWidth = class="code-number">2.0;Immutable Datenmodelle
Mutable State ist die Ursache einer überraschend großen Anzahl von Bugs. In meinen Projekten setze ich Immutability für alle Datenmodelle ausnahmslos durch.
class=class="code-string">"code-comment">// Schlecht - mutable, keine Gleichheit
class Product {
String name;
double price;
bool inStock;
Product(this.name, this.price, this.inStock);
}
class=class="code-string">"code-comment">// Gut - immutable, Wertgleichheit, einfaches Kopieren
class Product {
final String name;
final double price;
final bool inStock;
const Product({
required this.name,
required this.price,
required this.inStock,
});
Product copyWith({
String? name,
double? price,
bool? inStock,
}) {
return Product(
name: name ?? this.name,
price: price ?? this.price,
inStock: inStock ?? this.inStock,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Product &&
name == other.name &&
price == other.price &&
inStock == other.inStock;
@override
int get hashCode => Object.hash(name, price, inStock);
}Für größere Projekte empfehle ich `freezed`, um den Boilerplate-Code automatisch zu generieren.
Fehlerbehandlung
Verwenden Sie Exceptions niemals für den Kontrollfluss. Modellieren Sie erwartete Fehler explizit.
Eigene Exception-Hierarchie
class=class="code-string">"code-comment">// Schlecht - generische Exceptions mit String-Nachrichten
throw Exception(class="code-string">'Benutzer nicht gefunden');
throw Exception(class="code-string">'Netzwerkfehler');
class=class="code-string">"code-comment">// Gut - typisierte Exception-Hierarchie
sealed class AppException implements Exception {
final String message;
final StackTrace? stackTrace;
const AppException(this.message, [this.stackTrace]);
}
class NetworkException extends AppException {
final int? statusCode;
const NetworkException(super.message, {this.statusCode, super.stackTrace});
}
class ValidationException extends AppException {
final Map<String, String> fieldErrors;
const ValidationException(super.message, {required this.fieldErrors});
}
class NotFoundException extends AppException {
final String resourceType;
final String resourceId;
const NotFoundException(this.resourceType, this.resourceId)
: super(class="code-string">'$resourceType mit ID $resourceId nicht gefunden');
}Result-Typ-Muster
Für Operationen, die auf erwartete Weise fehlschlagen können, geben Sie ein Result zurück, statt eine Exception zu werfen.
sealed class Result<T> {
const Result();
}
class Success<T> extends Result<T> {
final T value;
const Success(this.value);
}
class Failure<T> extends Result<T> {
final AppException error;
const Failure(this.error);
}
class=class="code-string">"code-comment">// Verwendung
Future<Result<User>> fetchUser(String id) async {
try {
final response = await _api.get(class="code-string">'/users/$id');
return Success(User.fromJson(response.data));
} on DioException catch (e) {
return Failure(NetworkException(
class="code-string">'Benutzer konnte nicht geladen werden',
statusCode: e.response?.statusCode,
));
}
}
class=class="code-string">"code-comment">// Das Ergebnis konsumieren
final result = await fetchUser(class="code-string">'class="code-number">123');
switch (result) {
case Success(:final value):
showProfile(value);
case Failure(:final error):
showError(error.message);
}Extension Methods
Extensions halten Ihre Modelle sauber und ermöglichen gleichzeitig domänenspezifische Hilfsfunktionen.
extension StringValidation on String {
bool get isValidEmail =>
RegExp(rclass="code-string">'^[\w-\.]+@([\w-]+\.)+[\w-]{class="code-number">2,class="code-number">4}$').hasMatch(this);
bool get isStrongPassword =>
length >= class="code-number">8 &&
contains(RegExp(rclass="code-string">'[A-Z]')) &&
contains(RegExp(rclass="code-string">'[class="code-number">0-class="code-number">9]')) &&
contains(RegExp(rclass="code-string">'[!@#$%^&*]'));
String get capitalized =>
isEmpty ? this : class="code-string">'${this[class="code-number">0].toUpperCase()}${substring(class="code-number">1)}';
}
extension DateTimeFormatting on DateTime {
String get relativeTime {
final diff = DateTime.now().difference(this);
if (diff.inDays > class="code-number">365) return class="code-string">'vor ${diff.inDays ~/ class="code-number">365} J.';
if (diff.inDays > class="code-number">30) return class="code-string">'vor ${diff.inDays ~/ class="code-number">30} Mon.';
if (diff.inDays > class="code-number">0) return class="code-string">'vor ${diff.inDays} T.';
if (diff.inHours > class="code-number">0) return class="code-string">'vor ${diff.inHours} Std.';
if (diff.inMinutes > class="code-number">0) return class="code-string">'vor ${diff.inMinutes} Min.';
return class="code-string">'gerade eben';
}
}
extension IterableExtensions<T> on Iterable<T> {
T? firstWhereOrNull(bool Function(T) test) {
for (final element in this) {
if (test(element)) return element;
}
return null;
}
Map<K, List<T>> groupBy<K>(K Function(T) keyOf) {
final map = <K, List<T>>{};
for (final element in this) {
(map[keyOf(element)] ??= []).add(element);
}
return map;
}
}Sealed Classes und Pattern Matching
Dart 3 hat Sealed Classes und exhaustives Pattern Matching eingeführt. Das ist ein entscheidender Fortschritt für State Management und Domänenmodellierung.
UI-State modellieren
sealed class PageState<T> {
const PageState();
}
class Loading<T> extends PageState<T> {
const Loading();
}
class Loaded<T> extends PageState<T> {
final T data;
const Loaded(this.data);
}
class Error<T> extends PageState<T> {
final String message;
final VoidCallback? onRetry;
const Error(this.message, {this.onRetry});
}
class=class="code-string">"code-comment">// Exhaustive Switch - der Compiler warnt, wenn ein Fall fehlt
Widget buildPage(PageState<List<Product>> state) {
return switch (state) {
Loading() => const CircularProgressIndicator(),
Loaded(:final data) => ProductList(products: data),
Error(:final message, :final onRetry) => ErrorView(
message: message,
onRetry: onRetry,
),
};
}Domänenlogik modellieren
sealed class PaymentMethod {
const PaymentMethod();
}
class CreditCard extends PaymentMethod {
final String last4;
final String brand;
const CreditCard({required this.last4, required this.brand});
}
class BankTransfer extends PaymentMethod {
final String iban;
const BankTransfer({required this.iban});
}
class DigitalWallet extends PaymentMethod {
final String provider;
const DigitalWallet({required this.provider});
}
String describePayment(PaymentMethod method) => switch (method) {
CreditCard(:final brand, :final last4) => class="code-string">'$brand endet auf $last4',
BankTransfer(:final iban) => class="code-string">'Überweisung an $iban',
DigitalWallet(:final provider) => class="code-string">'Bezahlen mit $provider',
};Dart-3+-Features, die Sie verwenden sollten
Records
Leichtgewichtige, immutable Datenbündel ohne den Aufwand einer vollständigen Klasse.
class=class="code-string">"code-comment">// Vor Dart class="code-number">3 - eine Klasse oder List war nötig
class Pair<A, B> {
final A first;
final B second;
Pair(this.first, this.second);
}
class=class="code-string">"code-comment">// Dart class="code-number">3 - Records
(String, int) parseNameAge(String input) {
final parts = input.split(class="code-string">':');
return (parts[class="code-number">0], int.parse(parts[class="code-number">1]));
}
class=class="code-string">"code-comment">// Benannte Felder für mehr Klarheit
({String host, int port}) parseAddress(String address) {
final parts = address.split(class="code-string">':');
return (host: parts[class="code-number">0], port: int.parse(parts[class="code-number">1]));
}
class=class="code-string">"code-comment">// Destructuring
final (name, age) = parseNameAge(class="code-string">'Alice:class="code-number">30');
final (:host, :port) = parseAddress(class="code-string">'localhost:class="code-number">8080');If-Case und Switch-Expressions
class=class="code-string">"code-comment">// If-Case für Inline-Pattern-Matching
void processResponse(Object response) {
if (response case {class="code-string">'status': class="code-string">'ok', class="code-string">'data': Map data}) {
handleData(data);
}
if (response case [int first, int second, ...]) {
print(class="code-string">'Beginnt mit $first, $second');
}
}
class=class="code-string">"code-comment">// Switch-Expression - gibt einen Wert zurück
String httpStatus(int code) => switch (code) {
class="code-number">200 => class="code-string">'OK',
class="code-number">301 => class="code-string">'Dauerhaft verschoben',
class="code-number">404 => class="code-string">'Nicht gefunden',
>= class="code-number">500 && < class="code-number">600 => class="code-string">'Serverfehler',
_ => class="code-string">'Unbekannt',
};Klassen-Modifikatoren
class=class="code-string">"code-comment">// interface class - kann implementiert, aber nicht erweitert werden
interface class Authenticator {
Future<bool> authenticate(String token);
}
class=class="code-string">"code-comment">// base class - kann erweitert, aber nicht implementiert werden
base class Repository {
final Database _db;
Repository(this._db);
}
class=class="code-string">"code-comment">// final class - kann außerhalb der Bibliothek weder erweitert
class=class="code-string">"code-comment">// noch implementiert werden
final class AppConfig {
final String apiUrl;
final Duration timeout;
const AppConfig({required this.apiUrl, required this.timeout});
}
class=class="code-string">"code-comment">// mixin class - kann sowohl als Mixin als auch als Klasse verwendet werden
mixin class Loggable {
void log(String message) => print(class="code-string">'[${runtimeType}] $message');
}Häufige Dart-Anti-Patterns
Anti-Pattern: String-basierte Typisierung
class=class="code-string">"code-comment">// Schlecht - Fehler zeigen sich erst zur Laufzeit
void setTheme(String theme) {
if (theme == class="code-string">'dark') { class=class="code-string">"code-comment">/* ... */ }
else if (theme == class="code-string">'light') { class=class="code-string">"code-comment">/* ... */ }
}
setTheme(class="code-string">'drak'); class=class="code-string">"code-comment">// Tippfehler, kein Compilerfehler
class=class="code-string">"code-comment">// Gut - typsicher
enum AppTheme { dark, light, system }
void setTheme(AppTheme theme) => switch (theme) {
AppTheme.dark => activateDarkMode(),
AppTheme.light => activateLightMode(),
AppTheme.system => followSystemTheme(),
};Anti-Pattern: Gott-Klassen
class=class="code-string">"code-comment">// Schlecht - eine Klasse macht alles
class UserManager {
Future<User> fetchUser() async { class=class="code-string">"code-comment">/* ... */ }
void validateEmail(String email) { class=class="code-string">"code-comment">/* ... */ }
void sendPushNotification(String message) { class=class="code-string">"code-comment">/* ... */ }
void writeToAnalytics(String event) { class=class="code-string">"code-comment">/* ... */ }
Widget buildProfileCard() { class=class="code-string">"code-comment">/* ... */ }
}
class=class="code-string">"code-comment">// Gut - Einzelverantwortung
class UserRepository { class=class="code-string">"code-comment">/* Abruf, Cache */ }
class EmailValidator { class=class="code-string">"code-comment">/* Validierung */ }
class NotificationService { class=class="code-string">"code-comment">/* Push, lokal */ }
class AnalyticsTracker { class=class="code-string">"code-comment">/* Events */ }
class ProfileCard extends StatelessWidget { class=class="code-string">"code-comment">/* UI */ }Anti-Pattern: Alles abfangen
class=class="code-string">"code-comment">// Schlecht - verschluckt alle Fehler still
try {
await riskyOperation();
} catch (e) {
print(e);
}
class=class="code-string">"code-comment">// Gut - spezifische Typen abfangen, Unerwartetes weiterwerfen
try {
await riskyOperation();
} on NetworkException catch (e) {
showSnackbar(class="code-string">'Verbindungsfehler: ${e.message}');
} on ValidationException catch (e) {
showFieldErrors(e.fieldErrors);
} catch (e, stackTrace) {
class=class="code-string">"code-comment">// Unerwarteter Fehler - protokollieren und weiterwerfen
crashReporter.record(e, stackTrace);
rethrow;
}Anti-Pattern: Verschachtelte Futures
class=class="code-string">"code-comment">// Schlecht - Callback-Pyramide
fetchUser(id).then((user) {
fetchOrders(user.id).then((orders) {
fetchDetails(orders.first.id).then((details) {
updateUI(details);
});
});
});
class=class="code-string">"code-comment">// Gut - flaches async/await
final user = await fetchUser(id);
final orders = await fetchOrders(user.id);
final details = await fetchDetails(orders.first.id);
updateUI(details);Lint-Konfiguration
In meinen Projekten setze ich diese Regeln vom ersten Tag an durch. Eine strikte `analysis_options.yaml` fängt Probleme ab, bevor sie das Code-Review erreichen.
include: package:flutter_lints/flutter.yaml
analyzer:
errors:
missing_return: error
dead_code: warning
unused_import: warning
language:
strict-casts: true
strict-raw-types: true
linter:
rules:
# Stil
- always_declare_return_types
- annotate_overrides
- avoid_print
- prefer_const_constructors
- prefer_const_declarations
- prefer_final_fields
- prefer_final_locals
- sort_constructors_first
# Sicherheit
- avoid_dynamic_calls
- avoid_returning_null_for_future
- cancel_subscriptions
- close_sinks
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- throw_in_finally
- unnecessary_statements
- avoid_slow_async_io
# Lesbarkeit
- prefer_single_quotes
- require_trailing_commas
- use_named_constants
- unnecessary_lambdas
- prefer_expression_function_bodiesZusätzlich empfehle ich, regelmäßig `dart fix --apply` auszuführen, um triviale Lint-Probleme im gesamten Projekt automatisch zu beheben.
Fazit
Sauberer Dart-Code senkt die Einarbeitungszeit und die langfristigen Wartungskosten erheblich. Die hier vorgestellten Praktiken sind keine theoretischen Ideale, sondern Muster, die ich in jedem Projekt anwende. Beginnen Sie mit striktem Linting, setzen Sie Immutability durch, nutzen Sie Sealed Classes für die Domänenmodellierung und verwenden Sie Pattern Matching, wo immer es möglich ist. Ihr zukünftiges Ich und Ihre Teammitglieder werden es Ihnen danken.
Kontaktieren Sie mich, um teamweite Dart-Qualitätsstandards zu definieren und automatisierte Prüfungen in der CI einzurichten.
Verwandte Artikel
Flutter State Management: Riverpod, Provider und Bloc im Vergleich
Vergleichen Sie State-Management-Ansätze in Flutter. Verstehen Sie Riverpod, Provider und Bloc mit klaren Entscheidungskriterien.
Clean Architecture in Flutter: Skalierbare Anwendungen entwickeln
Lernen Sie, Clean Architecture in Flutter praxisnah umzusetzen. Ein Leitfaden für Schichten, Dependency Management und testbaren Code.
Flutter Performance-Optimierung: Vollständiger Leitfaden
Steigern Sie die Performance Ihrer Flutter-App systematisch. Lernen Sie Rebuild-Optimierung, Speichermanagement, Lazy Loading und Profiling.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen