flutter-patterns

Flutter architecture patterns: BLoC vs. Riverpod decision framework, Widget composition (Composition over Inheritance, CustomPainter, Implicit/Explicit Animations), go_router navigation, Platform Channels, Isolates for background work, Flutter DevTools profiling, performance patterns (const widgets, RepaintBoundary, ListView.builder, Slivers).

8 stars

Best use case

flutter-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Flutter architecture patterns: BLoC vs. Riverpod decision framework, Widget composition (Composition over Inheritance, CustomPainter, Implicit/Explicit Animations), go_router navigation, Platform Channels, Isolates for background work, Flutter DevTools profiling, performance patterns (const widgets, RepaintBoundary, ListView.builder, Slivers).

Teams using flutter-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/flutter-patterns/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/flutter-patterns/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/flutter-patterns/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How flutter-patterns Compares

Feature / Agentflutter-patternsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Flutter architecture patterns: BLoC vs. Riverpod decision framework, Widget composition (Composition over Inheritance, CustomPainter, Implicit/Explicit Animations), go_router navigation, Platform Channels, Isolates for background work, Flutter DevTools profiling, performance patterns (const widgets, RepaintBoundary, ListView.builder, Slivers).

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Flutter Patterns Skill

Flutter turns Dart into a cross-platform UI framework for iOS, Android, Web, and Desktop. This skill covers the architectural patterns and performance techniques that separate production apps from prototypes.

## When to Activate

- Designing state management for a new Flutter feature
- Setting up navigation with deep linking
- Calling native platform code from Flutter
- Running CPU-intensive work without blocking the UI
- Diagnosing Flutter performance issues (jank, rebuilds)
- Reviewing Flutter architecture choices
- Choosing between BLoC and Riverpod for a specific feature or team context
- Setting up Fastlane or GitHub Actions for Flutter iOS and Android CI/CD pipelines
- Optimizing widget rebuild scope using `const`, `RepaintBoundary`, or `ListView.builder`

---

## Widget Composition

Flutter's core paradigm: **Composition over Inheritance**.

```dart
// Build complex UI from small, reusable pieces
class ProductCard extends StatelessWidget {
  const ProductCard({required this.product, super.key});
  final Product product;

  @override
  Widget build(BuildContext context) => Card(
    child: Column(children: [
      ProductImage(url: product.imageUrl),      // separate widget
      ProductTitle(title: product.name),         // separate widget
      ProductPrice(price: product.price),        // separate widget
      AddToCartButton(productId: product.id),    // separate widget
    ]),
  );
}
```

**Never** use widget inheritance for UI variation — use parameters or slots instead.

---

## State Management

### BLoC (flutter_bloc)

Best for: complex features, large teams, explicit event-driven flows.

```dart
// 1. Define events (sealed class — Dart 3)
sealed class CartEvent {}
class AddToCart extends CartEvent { final Product product; AddToCart(this.product); }
class RemoveFromCart extends CartEvent { final String productId; RemoveFromCart(this.productId); }

// 2. Define state (freezed for copyWith + equality)
@freezed
class CartState with _$CartState {
  const factory CartState({
    @Default([]) List<CartItem> items,
    @Default(false) bool isLoading,
    CartError? error,
  }) = _CartState;
}

// 3. BLoC
class CartBloc extends Bloc<CartEvent, CartState> {
  CartBloc(this._repository) : super(const CartState()) {
    on<AddToCart>(_onAddToCart);
    on<RemoveFromCart>(_onRemoveFromCart);
  }

  Future<void> _onAddToCart(AddToCart e, Emitter<CartState> emit) async {
    emit(state.copyWith(isLoading: true, error: null));
    try {
      final items = await _repository.addItem(state.items, e.product);
      emit(state.copyWith(items: items, isLoading: false));
    } on CartException catch (err) {
      emit(state.copyWith(error: CartError(err.message), isLoading: false));
    }
  }
}

// 4. Widget
class CartScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) => BlocBuilder<CartBloc, CartState>(
    builder: (context, state) {
      if (state.isLoading) return const CircularProgressIndicator();
      return ListView(children: state.items.map(CartItemTile.new).toList());
    },
  );
}
```

### Riverpod

Best for: simpler features, reactive providers, less boilerplate.

```dart
// Provider (auto-disposed, reacts to dependencies)
@riverpod
Future<List<Product>> featuredProducts(FeaturedProductsRef ref) {
  return ref.watch(productRepositoryProvider).getFeatured();
}

// Notifier for mutable state
@riverpod
class CartNotifier extends _$CartNotifier {
  @override
  List<CartItem> build() => [];

  void add(Product product) {
    state = [...state, CartItem(product: product, quantity: 1)];
  }
}

// Widget
class FeaturedScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final products = ref.watch(featuredProductsProvider);
    return products.when(
      data: (list) => ProductGrid(items: list),
      loading: () => const SkeletonGrid(),
      error: (err, _) => ErrorCard(message: err.toString()),
    );
  }
}
```

---

## Navigation (go_router)

```dart
final router = GoRouter(
  initialLocation: '/',
  redirect: (context, state) {
    final auth = context.read<AuthBloc>().state;
    final onAuth = state.matchedLocation.startsWith('/auth');
    if (auth is Unauthenticated && !onAuth) return '/auth/login';
    if (auth is Authenticated && onAuth) return '/';
    return null;
  },
  routes: [
    ShellRoute(
      builder: (_, __, child) => AppScaffold(body: child),
      routes: [
        GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
        GoRoute(
          path: '/product/:id',
          builder: (_, s) => ProductScreen(id: s.pathParameters['id']!),
        ),
      ],
    ),
    GoRoute(path: '/auth/login', builder: (_, __) => const LoginScreen()),
  ],
);

// Navigate programmatically
context.go('/product/123');     // replace stack
context.push('/product/123');   // push onto stack
context.pop();                  // back
```

---

## Platform Channels

Call native iOS/Android code from Dart when Flutter APIs don't cover it.

```dart
// Dart side
const _channel = MethodChannel('com.myapp/native');

Future<String> getBiometricType() async {
  try {
    return await _channel.invokeMethod<String>('getBiometricType') ?? 'none';
  } on PlatformException catch (e) {
    debugPrint('Biometric error: ${e.message}');
    return 'error';
  }
}

// iOS Swift side (AppDelegate.swift)
let channel = FlutterMethodChannel(name: "com.myapp/native", binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { call, result in
  if call.method == "getBiometricType" {
    result(LAContext().biometryType == .faceID ? "faceID" : "touchID")
  }
}
```

Channel types:
- `MethodChannel` — request/response (most common)
- `EventChannel` — continuous stream from native (sensors, connectivity)
- `BasicMessageChannel` — bidirectional low-level messaging

---

## Isolates (Background Work)

Flutter's main isolate handles UI. CPU-intensive work must move off it.

```dart
// Simple: compute() for pure functions
final sorted = await compute(sortLargeList, items);

// Complex: Isolate.spawn for long-running work
Future<void> runInBackground() async {
  final receivePort = ReceivePort();
  await Isolate.spawn(_isolateEntryPoint, receivePort.sendPort);

  await for (final message in receivePort) {
    if (message is ProcessingResult) {
      // update UI
    }
  }
}

void _isolateEntryPoint(SendPort sendPort) {
  // This runs in a separate isolate (separate memory, no shared state)
  final result = expensiveComputation();
  sendPort.send(result);
}
```

---

## Performance Patterns

### `const` widgets (highest impact)

Every `const` widget is created once and never rebuilt. Missing `const` is a performance regression.

```dart
// Rule: if widget has no runtime variables, it should be const
return const Column(children: [
  Icon(Icons.star),          // const
  Text('Featured'),          // const
  SizedBox(height: 8),       // const
]);
```

### RepaintBoundary

Isolate expensive widgets from rebuild cascade:

```dart
// Animation inside doesn't trigger parent repaint
RepaintBoundary(
  child: AnimatedProgressRing(value: progress),
)
```

### ListView.builder (never ListView with children for long lists)

```dart
// WRONG: creates all items eagerly (even off-screen)
ListView(children: items.map(ItemTile.new).toList())

// CORRECT: lazy creation (only visible + buffer)
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, i) => ItemTile(item: items[i]),
)
```

### Slivers for complex scrolling

```dart
CustomScrollView(slivers: [
  const SliverAppBar(expandedHeight: 200, floating: true, pinned: true),
  SliverList(
    delegate: SliverChildBuilderDelegate(
      (context, i) => ProductTile(product: products[i]),
      childCount: products.length,
    ),
  ),
])
```

---

## Flutter DevTools

```bash
flutter run                     # Start app in debug mode
# Press 'd' in terminal or open DevTools URL

flutter pub global activate devtools
flutter pub global run devtools
```

Key DevTools views:
- **Performance**: Frame timeline, identify jank (>16ms frames)
- **Widget Inspector**: Widget tree, layout constraints, repaint highlights
- **Memory**: Heap allocation, detect memory leaks
- **Network**: HTTP requests, response times

---

## Mobile CI/CD for Flutter

Flutter apps target both iOS and Android from one codebase, but each platform has its own code-signing and distribution requirements.

### Build Commands

```bash
# Android — release App Bundle (required for Play Store)
flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info/

# iOS — release archive (requires macOS + Xcode)
flutter build ipa --release --export-options-plist=ios/ExportOptions.plist

# Web
flutter build web --release --base-href /myapp/
```

### Fastlane Integration

Fastlane works for Flutter apps the same way it does for native apps. Flutter builds produce standard `.ipa` and `.aab` artefacts that Fastlane can sign and upload.

```ruby
# fastlane/Fastfile
platform :ios do
  lane :beta do
    match(type: "appstore", readonly: is_ci)
    sh("flutter build ipa --release --export-options-plist=../ios/ExportOptions.plist")
    upload_to_testflight(
      ipa: "build/ios/ipa/MyApp.ipa",
      skip_waiting_for_build_processing: true,
    )
  end
end

platform :android do
  lane :beta do
    sh("flutter build appbundle --release")
    upload_to_play_store(
      track: "internal",
      aab: "build/app/outputs/bundle/release/app-release.aab",
      json_key: ENV["PLAY_STORE_SERVICE_ACCOUNT_JSON"],
    )
  end
end
```

### Expo EAS for Flutter? No — Use Fastlane or Codemagic

EAS Build is Expo-specific (React Native). For Flutter, use:
- **Fastlane** — free, open source, full control
- **Codemagic** — Flutter-native CI service with built-in signing management
- **GitHub Actions** — with `subosito/flutter-action@v2`

### GitHub Actions — Flutter CI Matrix

```yaml
# .github/workflows/flutter-ci.yml
name: Flutter CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.x', channel: 'stable', cache: true }
      - run: flutter pub get
      - run: flutter analyze
      - run: flutter test --coverage
      - uses: codecov/codecov-action@v4
        with: { file: coverage/lcov.info }

  build-android:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { java-version: '21', distribution: 'temurin' }
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.x', channel: 'stable', cache: true }
      - run: flutter pub get
      - name: Decode keystore
        run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore
      - run: flutter build appbundle --release
        env:
          ANDROID_KEYSTORE_PATH: release.keystore
          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
          ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
      - name: Upload to Play Store
        run: bundle exec fastlane android beta

  build-ios:
    runs-on: macos-15
    needs: test
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.x', channel: 'stable', cache: true }
      - run: flutter pub get
      - run: cd ios && pod install
      - name: Sync iOS signing
        run: bundle exec fastlane sync_signing
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_TOKEN }}
      - run: flutter build ipa --release
      - run: bundle exec fastlane ios beta
```

### Version Management

Flutter uses `pubspec.yaml` for versioning:

```yaml
# pubspec.yaml — format: version_name+build_number
version: 2.1.0+47  # "2.1.0" displayed to users; "47" is build number (monotonically increasing)
```

Auto-update build number from CI:

```bash
# In CI: replace build number with git commit count
BUILD_NUMBER=$(git rev-list HEAD --count)
sed -i "s/^version: .*/version: 2.1.0+${BUILD_NUMBER}/" pubspec.yaml
```

---

## Reference Rules

- `rules/flutter/coding-style.md` — dart format, const correctness, naming
- `rules/flutter/patterns.md` — architecture decisions, Clean Architecture layers
- `rules/flutter/testing.md` — widget tests, golden tests, BLoC testing
- Skill: `mobile-cicd-patterns` — detailed code signing, Fastlane, TestFlight, Firebase App Distribution

Related Skills

zero-trust-patterns

8
from marvinrichter/clarc

Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.

webrtc-patterns

8
from marvinrichter/clarc

WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.

webhook-patterns

8
from marvinrichter/clarc

Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.

wasm-patterns

8
from marvinrichter/clarc

WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).

ux-micro-patterns

8
from marvinrichter/clarc

UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.

typescript-patterns

8
from marvinrichter/clarc

TypeScript patterns — type system best practices, strict mode, utility types, generics, discriminated unions, error handling with Result types, and module organization. Core patterns for production TypeScript.

typescript-patterns-advanced

8
from marvinrichter/clarc

Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.

typescript-monorepo-patterns

8
from marvinrichter/clarc

TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.

terraform-patterns

8
from marvinrichter/clarc

Infrastructure as Code with Terraform — project structure, remote state, modules, workspace strategy, AWS/GCP patterns, CI/CD integration, and security hardening. The standard for managing production infrastructure.

swiftui-patterns

8
from marvinrichter/clarc

SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.

swift-patterns

8
from marvinrichter/clarc

Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development.

swift-patterns-advanced

8
from marvinrichter/clarc

Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.