Communitygithub.com

dart-flutter-patterns

Produktionsreife Dart- und Flutter-Muster mit Fokus auf Null-Sicherheit, unveränderlichen Zustand, asynchrone Komposition, Widget-Architektur, gängige State-Management-Frameworks (BLoC, Riverpod, Provider), GoRouter-Navigation, Dio-Netzwerk, Freezed-Codegenerierung und saubere Architektur. Verwenden Sie diesen Skill beim Schreiben oder Überprüfen von Dart- und Flutter-Code – für Zustand, Widgets, Navigation, Netzwerk oder Architektur.

Was ist dart-flutter-patterns?

Dieser Skill bietet eine umfassende Sammlung von produktionsreifen Code-Mustern für Dart- und Flutter-Anwendungen. Er behandelt die grundlegenden Aspekte der modernen Flutter-Entwicklung: von Null-Sicherheit über unveränderlichen Zustand bis hin zu asynchroner Komposition und Widget-Architektur. Der Skill ist besonders nützlich für Entwickler, die State-Management-Frameworks wie BLoC, Riverpod oder Provider einsetzen. Er enthält konkrete Codebeispiele für typische Aufgaben wie die Navigation mit GoRouter inklusive reaktiver Authentifizierungs-Guards, die HTTP-Kommunikation mit Dio inklusive Token-Refresh, die Codegenerierung mit Freezed und die Test-Implementierung. Die Muster sind bewusst als universelle Vorlagen gestaltet – sie sind direkt kopierbar und an das eigene Projekt anpassbar. Dadurch eignet sich der Skill sowohl für das Einrichten neuer Projekte als auch für die Code-Review oder das Refactoring bestehender Anwendungen und trägt zur Einhaltung von Best Practices bei.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/dart-flutter-patterns

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Dart/Flutter-Muster

Wann verwenden

Verwenden Sie diesen Skill, wenn:

  • Sie eine neue Flutter-Funktion starten und idiomatische Muster für State-Management, Navigation oder Datenzugriff benötigen
  • Sie Dart-Code überprüfen oder schreiben und eine Orientierung zu Null-Sicherheit, versiegelten Typen oder asynchroner Komposition benötigen
  • Sie ein neues Flutter-Projekt einrichten und zwischen BLoC, Riverpod oder Provider wählen
  • Sie sichere HTTP-Clients, WebView-Integration oder lokale Speicherung implementieren
  • Sie Tests für Flutter-Widgets, Cubits oder Riverpod-Provider schreiben
  • Sie GoRouter mit Authentifizierungs-Guards verbinden

Wie es funktioniert

Dieser Skill bietet direkt kopierbare Dart/Flutter-Code-Muster, die nach Themenbereichen organisiert sind:

  1. Null-Sicherheit – vermeiden Sie !, bevorzugen Sie ?./??/Pattern Matching
  2. Unveränderlicher Zustand – versiegelte Klassen, freezed, copyWith
  3. Asynchrone Komposition – gleichzeitiges Future.wait, sicheres BuildContext nach await
  4. Widget-Architektur – in Klassen extrahieren (nicht Methoden), const-Propagation, isolierte Neuerstellungen
  5. State-Management – BLoC/Cubit-Ereignisse, Riverpod-Notifier und abgeleitete Provider
  6. Navigation – GoRouter mit reaktiven Auth-Guards via refreshListenable
  7. Netzwerk – Dio mit Interceptors, Token-Refresh mit einmaliger Wiederholungsabsicherung
  8. Fehlerbehandlung – globale Erfassung, ErrorWidget.builder, Crashlytics-Anbindung
  9. Testen – Unit-Tests (BLoC-Test), Widget-Tests (ProviderScope-Überschreibungen), Fakes statt Mocks

Beispiele

// Versiegelter Zustand – verhindert unmögliche Zustände
sealed class AsyncState<T> {}
final class Loading<T> extends AsyncState<T> {}
final class Success<T> extends AsyncState<T> { final T data; const Success(this.data); }
final class Failure<T> extends AsyncState<T> { final Object error; const Failure(this.error); }

// GoRouter mit reaktiver Auth-Umleitung
final router = GoRouter(
  refreshListenable: GoRouterRefreshStream(authCubit.stream),
  redirect: (context, state) {
    final authed = context.read<AuthCubit>().state is AuthAuthenticated;
    if (!authed && !state.matchedLocation.startsWith('/login')) return '/login';
    return null;
  },
  routes: [...],
);

// Riverpod abgeleiteter Provider mit sicherem firstWhereOrNull
@riverpod
double cartTotal(Ref ref) {
  final cart = ref.watch(cartNotifierProvider);
  final products = ref.watch(productsProvider).valueOrNull ?? [];
  return cart.fold(0.0, (total, item) {
    final product = products.firstWhereOrNull((p) => p.id == item.productId);
    return total + (product?.price ?? 0) * item.quantity;
  });
}

Praktische, produktionsreife Muster für Dart- und Flutter-Anwendungen. Bibliotheksunabhängig, wo möglich, mit expliziter Abdeckung der gängigsten Ökosystempakete.


1. Grundlagen der Null-Sicherheit

Muster gegenüber Bang-Operator bevorzugen

// SCHLECHT – stürzt zur Laufzeit bei null ab
final name = user!.name;

// GUT – Fallback bereitstellen
final name = user?.name ?? 'Unknown';

// GUT – Dart 3 Pattern Matching (bevorzugt für komplexe Fälle)
final display = switch (user) {
  User(:final name, :final email) => '$name <$email>',
  null => 'Guest',
};

// GUT – frühe Rückkehr absichern
String getUserName(User? user) {
  if (user == null) return 'Unknown';
  return user.name; // nach Prüfung zu non-null befördert
}

Übermäßige Verwendung von late vermeiden

// SCHLECHT – verschiebt Null-Fehler zur Laufzeit
late String userId;

// GUT – nullable mit expliziter Initialisierung
String? userId;

// OK – late nur verwenden, wenn die Initialisierung vor dem ersten Zugriff garantiert ist
// (z. B. in initState() vor jeglicher Widget-Interaktion)
late final AnimationController _controller;

@override
void initState() {
  super.initState();
  _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
}

2. Unveränderlicher Zustand

Versiegelte Klassen für Zustandshierarchien

sealed class UserState {}

final class UserInitial extends UserState {}

final class UserLoading extends UserState {}

final class UserLoaded extends UserState {
  const UserLoaded(this.user);
  final User user;
}

final class UserError extends UserState {
  const UserError(this.message);
  final String message;
}

// Erschöpfendes Switch – Compiler erzwingt alle Zweige
Widget buildFrom(UserState state) => switch (state) {
  UserInitial() => const SizedBox.shrink(),
  UserLoading() => const CircularProgressIndicator(),
  UserLoaded(:final user) => UserCard(user: user),
  UserError(:final message) => ErrorText(message),
};

Freezed für boilerplate-freie Unveränderlichkeit

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    required String email,
    @Default(false) bool isAdmin,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

// Verwendung
final user = User(id: '1', name: 'Alice', email: '[email protected]');
final updated = user.copyWith(name: 'Alice Smith'); // unveränderliches Update
final json = user.toJson();
final fromJson = User.fromJson(json);

3. Asynchrone Komposition

Strukturierte Nebenläufigkeit mit Future.wait

Future<DashboardData> loadDashboard(UserRepository users, OrderRepository orders) async {
  // Gleichzeitig ausführen – nicht sequenziell abwarten
  final (userList, orderList) = await (
    users.getAll(),
    orders.getRecent(),
  ).wait; // Dart 3 Record-Destrukturierung + Future.wait-Erweiterung

  return DashboardData(users: userList, orders: orderList);
}

Stream-Muster

// Repository stellt reaktive Streams für Live-Daten bereit
Stream<List<Item>> watchCartItems() => _db
    .watchTable('cart_items')
    .map((rows) => rows.map(Item.fromRow).toList());

// In der Widget-Ebene – deklarativ, kein manuelles Abonnement
StreamBuilder<List<Item>>(
  stream: cartRepository.watchCartItems(),
  builder: (context, snapshot) => switch (snapshot) {
    AsyncSnapshot(connectionState: ConnectionState.waiting) =>
        const CircularProgressIndicator(),
    AsyncSnapshot(:final error?) => ErrorWidget(error.toString()),
    AsyncSnapshot(:final data?) => CartList(items: data),
    _ => const SizedBox.shrink(),
  },
)

BuildContext nach Await

// KRITISCH – nach jedem await in StatefulWidget immer 'mounted' prüfen
Future<void> _handleSubmit() async {
  setState(() => _isLoading = true);
  try {
    await authService.login(_email, _password);
    if (!mounted) return; // ← Absicherung vor Verwendung von context
    context.go('/home');
  } on AuthException catch (e) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message)));
  } finally {
    if (mounted) setState(() => _isLoading = false);
  }
}

4. Widget-Architektur

In Klassen extrahieren, nicht in Methoden

// SCHLECHT – private Methode, die ein Widget zurückgibt, verhindert Optimierung
Widget _buildHeader() {
  return Container(
    padding: const EdgeInsets.all(16),
    child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
  );
}

// GUT – separate Widget-Klasse, ermöglicht const, Element-Wiederverwendung
class _PageHeader extends StatelessWidget {
  const _PageHeader(this.title);
  final String title;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
    );
  }
}

const-Propagation

// SCHLECHT – neue Instanzen bei jedem Neuaufbau
child: Padding(
  padding: EdgeInsets.all(16.0),       // nicht const
  child: Icon(Icons.home, size: 24.0), // nicht const
)

// GUT – const stoppt die Neuaufbau-Propagation
child: const Padding(
  padding: EdgeInsets.all(16.0),
  child: Icon(Icons.home, size: 24.0),
)

Isolierte Neuerstellungen

// SCHLECHT – gesamte Seite wird bei jeder Zähleränderung neu erstellt
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider); // baut alles neu
    return Scaffold(
      body: Column(children: [
        const ExpensiveHeader(), // unnötig neu erstellt
        Text('$count'),
        const ExpensiveFooter(), // unnötig neu erstellt
      ]),
    );
  }
}

// GUT – den neu zu erstellenden Teil isolieren
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Column(children: [
        ExpensiveHeader(),        // wird nie neu erstellt (const)
        _CounterDisplay(),        // nur dieses wird neu erstellt
        ExpensiveFooter(),        // wird nie neu erstellt (const)
      ]),
    );
  }
}

class _CounterDisplay extends ConsumerWidget {
  const _CounterDisplay();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

5. State-Management: BLoC/Cubit

// Cubit – synchroner oder einfacher asynchroner Zustand
class AuthCubit extends Cubit<AuthState> {
  AuthCubit(this._authService) : super(const AuthState.initial());
  final AuthService _authService;

  Future<void> login(String email, String password) async {
    emit(const AuthState.loading());
    try {
      final user = await _authService.login(email, password);
      emit(AuthState.authenticated(user));
    } on AuthException catch (e) {
      emit(AuthState.error(e.message));
    }
  }

  void logout() {
    _authService.logout();
    emit(const AuthState.initial());
  }
}

// Im Widget
BlocBuilder<AuthCubit, AuthState>(
  builder: (context, state) => switch (state) {
    AuthInitial() => const LoginForm(),
    AuthLoading() => const CircularProgressIndicator(),
    AuthAuthenticated(:final user) => HomePage(user: user),
    AuthError(:final message) => ErrorView(message: message),
  },
)

6. State-Management: Riverpod

// Auto-dispose async provider
@riverpod
Future<List<Product>> products(Ref ref) async {
  final repo = ref.watch(productRepositoryProvider);
  return repo.getAll();
}

// Notifier mit komplexen Mutationen
@riverpod
class CartNotifier extends _$CartNotifier {
  @override
  List<CartItem> build() => [];

  void add(Product product) {
    final existing = state.where((i) => i.productId == product.id).firstOrNull;
    if (existing != null) {
      state = [
        for (final item in state)
          if (item.productId == product.id) item.copyWith(quantity: item.quantity + 1)
          else item,
      ];
    } else {
      state = [...state, CartItem(productId: product.id, quantity: 1)];
    }
  }

  void remove(String productId) =>
      state = state.where((i) => i.productId != productId).toList();

  void clear() => state = [];
}

// Abgeleiteter Provider (Selector-Muster)
@riverpod
int cartCount(Ref ref) => ref.watch(cartNotifierProvider).length;

@riverpod
double cartTotal(Ref ref) {
  final cart = ref.watch(cartNotifierProvider);
  final products = ref.watch(productsProvider).valueOrNull ?? [];
  return cart.fold(0.0, (total, item) {
    // firstWhereOrNull (aus dem collection-Paket) vermeidet StateError, wenn ein Produkt fehlt
    final product = products.firstWhereOrNull((p) => p.id == item.productId);
    return total + (product?.price ?? 0) * item.quantity;
  });
}

7. Navigation mit GoRouter

final router = GoRouter(
  initialLocation: '/',
  // refreshListenable wertet die Umleitung bei jeder Änderung des Auth-Status neu aus
  refreshListenable: GoRouterRefreshStream(authCubit.stream),
  redirect: (context, state) {
    final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated;
    final isGoingToLogin = state.matchedLocation == '/login';
    if (!isLoggedIn && !isGoingToLogin) return '/login';
    if (isLoggedIn && isGoingToLogin) return '/';
    return null;
  },
  routes: [
    GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
    ShellRoute(
      builder: (context, state, child) => AppShell(child: child),
      routes: [
        GoRoute(path: '/', builder: (_, __) => const HomePage()),
        GoRoute(
          path: '/products/:id',
          builder: (context, state) =>
              ProductDetailPage(id: state.pathParameters['id']!),
        ),
      ],
    ),
  ],
);

8. HTTP mit Dio

final dio = Dio(BaseOptions(
  baseUrl: const String.fromEnvironment('API_URL'),
  connectTimeout: const Duration(seconds: 10),
  receiveTimeout: const Duration(seconds: 30),
  headers: {'Content-Type': 'application/json'},
));

// Auth-Interceptor hinzufügen
dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) async {
    final token = await secureStorage.read(key: 'auth_token');
    if (token != null) options.headers['Authorization'] = 'Bearer $token';
    handler.next(options);
  },
  onError: (error, handler) async {
    // Absicherung gegen unendliche Wiederholungsschleifen: nur ein Mal pro Anfrage versuchen
    final isRetry = error.requestOptions.extra['_isRetry'] == true;
    if (!isRetry && error.response?.statusCode == 401) {
      final refreshed = await attemptTokenRefresh();
      if (refreshed) {
        error.requestOptions.extra['_isRetry'] = true;
        return handler.resolve(await dio.fetch(error.requestOptions));
      }
    }
    handler.next(error);
  },
));

// Repository mit Dio
class UserApiDataSource {
  const UserApiDataSource(this._dio);
  final Dio _dio;

  Future<User> getById(String id) async {
    final response = await _dio.get<Map<String, dynamic>>('/users/$id');
    return User.fromJson(response.data!);
  }
}

9. Fehlerbehandlungs-Architektur

// Globale Fehlererfassung – in main() einrichten
void main() {
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    crashlytics.recordFlutterFatalError(details);
  };

  PlatformDispatcher.instance.onError = (error, stack) {
    crashlytics.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const App());
}

// Benutzerdefiniertes ErrorWidget für Produktion
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, widget) {
        ErrorWidget.builder = (details) => 
          Material(
            child: Center(
              child: Text(
                'Etwas ist schiefgelaufen',
                style: Theme.of(context).textTheme.titleLarge,
              ),
            ),
          );
        return widget!;
      },
      home: const HomePage(),
    );
  }
}

10. Testen

BLoC/Cubit-Tests

void main() {
  group('AuthCubit', () {
    late AuthService mockAuthService;
    late AuthCubit cubit;

    setUp(() {
      mockAuthService = MockAuthService();
      cubit = AuthCubit(mockAuthService);
    });

    tearDown(() => cubit.close());

    test('gibt initial den Initial-Zustand aus', () {
      expect(cubit.state, const AuthState.initial());
    });

    blocTest<AuthCubit, AuthState>(
      'gibt [Loading, Authenticated] bei erfolgreichem Login aus',
      build: () => cubit,
      act: (cubit) => cubit.login('email', 'password'),
      setUp: () => when(mockAuthService.login(any, any))
          .thenAnswer((_) async => User(id: '1', name: 'Alice', email: '[email protected]')),
      expect: () => [
        const AuthState.loading(),
        isA<AuthAuthenticated>(),
      ],
    );
  });
}

Widget-Tests mit Riverpod

testWidgets('Cart-Add-Button fügt Element hinzu', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        productRepositoryProvider.overrideWithValue(FakeProductRepository()),
      ],
      child: const MaterialApp(home: ProductListPage()),
    ),
  );

  await tester.tap(find.text('In den Warenkorb'));
  await tester.pump();

  expect(find.text('1'), findsOneWidget); // Warenkorbzähler
});

Praktische, produktionsreife Muster für Dart und Flutter. Verwenden Sie dieselben Muster konsistent – das reduziert kognitive Belastung und macht Code vorhersagbarer.

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

Verwandte Skills