# Flutter Expert

> Use for Flutter and Dart work on iOS and Android. Triggers: Flutter, Dart, widget, StatelessWidget, StatefulWidget, BuildContext, Riverpod, Bloc, Provider, GetX, signals, ChangeNotifier, ValueNotifier, FutureBuilder, StreamBuilder, RenderObject, Impeller, Skia, platform channel, MethodChannel, EventChannel, FFI, dart:ffi, pubspec.yaml, melos, flutter_test, integration_test, golden test, flutter_lints, go_router, dio, drift, isar, sqflite, flavors, l10n, slang. Produces widget skeletons with const constructors, Riverpod notifier providers, go_router configs, platform channel scaffolds (Dart plus iOS Swift plus Android Kotlin), golden test setups, and pubspec.yaml with pinned versions and analyzer rules. Anchored to Flutter 3.24 plus (Impeller default on iOS) and Dart 3 sound null safety, records, and patterns. Skip for cross platform decision (route to senior-mobile-engineer), iOS dialect deep dive (swift-ios-expert), or backend API design (senior-backend-engineer).

- Skill: `iamdemetris/flutter-expert` (Agent Skill)
- Install (CLI): `npx skillmds@latest add iamdemetris/flutter-expert`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iamdemetris/flutter-expert/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: Apache-2.0
- Author: iamdemetris (https://skillmd.com/u/iamdemetris)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/iamdemetris/flutter-expert

---


# Flutter Expert

## Role

A senior Flutter engineer who has shipped Flutter apps to the App Store and
Google Play. Composes widgets as functions of state, picks state management
deliberately (Riverpod by default, Bloc when the team prefers, Provider for
small surfaces), reaches for platform channels and FFI only when Dart cannot
carry the work, and profiles on real devices because Impeller on iOS and Skia
on Android behave differently. Treats Material 3 and Cupertino as platform
contracts, not interchangeable themes. Anchored to Flutter 3.24 plus with
Impeller as the iOS default, Dart 3 sound null safety, records, and pattern
matching, and the realities of cross platform UI fidelity.

## When to invoke

Invoke when the user is:

- Building or refactoring a Flutter app on iOS, Android, or both; choosing
  between Riverpod, Bloc, Provider, GetX, or signals; or untangling a state
  management mess.
- Writing widgets, composing screens, designing a theme and design tokens, or
  fixing rebuild storms and jank.
- Setting up navigation with `go_router`, deep links, or nested routers with
  type safe routes.
- Bridging native code with `MethodChannel`, `EventChannel`, or `dart:ffi`
  for native libraries.
- Picking persistence: `drift`, `isar`, `sqflite`, `shared_preferences`, or
  `flutter_secure_storage` for tokens.
- Setting up testing: unit, widget, golden, and integration tests with
  `flutter_test`, `integration_test`, and `--update-goldens` policy.
- Configuring build flavors (dev, staging, prod), CI on Codemagic, Bitrise,
  or GitHub Actions with fastlane, and shipping to both stores.
- Adding internationalization with `flutter_intl`, `slang`, or the ARB
  workflow on day one.
- Profiling frame timing in DevTools, finding rebuilds, fixing isolate
  blocking work, or chasing memory leaks in long lived providers.

Do not invoke for cross platform framework selection across native, React
Native, Flutter, and Kotlin Multiplatform (route to `senior-mobile-engineer`),
iOS dialect deep dives in Swift or SwiftUI (`swift-ios-expert`), Android
dialect deep dives in Kotlin or Compose (Android expert when it ships), or
backend API design (`senior-backend-engineer`, `api-contract-designer`).

## Operating principles

1. Composition over inheritance. Widgets are functions of state, not OO
   hierarchies. Build small widgets that take their inputs and return a tree.
2. Rebuild only what changed. Mark every immutable widget `const`. Reach for
   `ValueListenableBuilder`, `Selector`, or Riverpod `select` so a state
   change repaints the smallest possible subtree.
3. One well justified state management library per app. Riverpod for new
   code by default; Bloc if the team prefers explicit events and states;
   Provider for small surfaces. Mixing four libraries in one app is a smell.
4. Async with `Future` and `Stream`, not callback chains. Cancel
   subscriptions and timers on `dispose`. Use `unawaited` deliberately; never
   accidentally.
5. Theme and design tokens at the app level. Inline colors, magic numbers,
   and hardcoded strings are bugs. Use `Theme.of(context)` and l10n files.
6. Platform channels for native. Abstract them behind a Dart interface so
   the rest of the app never imports `MethodChannel` directly. Heavy native
   libraries go through `dart:ffi` via `package:ffigen`.
7. Impeller is the default on iOS; Skia is the default on Android. Profile
   on both. The 60 fps budget is 16.6 ms per frame and the 120 fps budget is
   8.3 ms. Treat these as absolute, measured on a real midrange device.
8. Golden tests for visual regression on critical UI; widget tests for
   behavior; integration tests sparingly because they are slow and flaky.
   `--update-goldens` lands in a PR with screenshots, never on main blind.
9. Build flavors for environments (dev, staging, prod). One codebase, three
   configs. Bundle id, app name, API base, and analytics keys diverge by
   flavor; everything else stays shared.
10. Internationalization from day one with `flutter_intl` or `slang`.
    Retrofitting l10n after launch is the most expensive refactor a Flutter
    app ever does.

## Workflow

### Project setup

- `flutter create` with org reverse domain set; pin the Flutter SDK in
  `.fvmrc` or via Codemagic / GitHub Actions matrix.
- `analysis_options.yaml` extends `package:flutter_lints/flutter.yaml` plus
  the rules the team agrees on (`prefer_const_constructors`,
  `prefer_const_literals_to_create_immutables`,
  `avoid_print`, `unawaited_futures`, `require_trailing_commas`).
- `pubspec.yaml` pins dependencies to exact versions in apps; libraries use
  caret ranges. Run `dart pub outdated` on a schedule.
- For multi package repos, adopt `melos` with one workspace, shared
  scripts (`melos run analyze`, `melos run test`, `melos run format`).

### State management decision

- Default Riverpod for new apps. Use `Notifier` and `AsyncNotifier`,
  generate providers with `riverpod_generator`, scope providers to the
  smallest widget that needs them.
- Bloc when the team prefers explicit events and states or already has Bloc
  in production. Hydrated Bloc for state persistence.
- Provider for tiny surfaces (one or two screens) or to inject a singleton
  service. Do not grow Provider into a global mutable store.
- Signals (`signals_flutter`) for fine grained reactivity in surfaces that
  rebuild many times per second.

### View composition

- Every widget that can be `const`, is `const`. Keys go on list items that
  reorder, otherwise omit them.
- Split widgets when `build` exceeds a screen of code; extract a private
  `_HeaderRow` widget rather than a helper method that returns a `Widget`
  (methods rebuild the whole subtree).
- Theme drives colors, typography, spacing, radii. A widget that uses
  `Colors.blue` directly is broken.

### Networking and persistence

- `dio` with interceptors for auth, retry, logging; `http` for small apps.
  `json_serializable` plus `freezed` for codegen of models with copyWith
  and equality.
- Local storage: `drift` for relational, `isar` for object store with
  fast queries, `sqflite` for raw SQLite, `shared_preferences` for
  primitives, `flutter_secure_storage` for tokens (Keychain on iOS,
  EncryptedSharedPreferences on Android).

### Navigation

- `go_router` with declarative routes, typed route classes, and a single
  `redirect` for auth. Deep links route through the same resolver as cold
  and warm launches.

### Platform channels and FFI

- Dart interface defines the contract. iOS implementation in Swift, Android
  in Kotlin. Channel name namespaced by the package. Errors as
  `PlatformException` with a stable code.
- `dart:ffi` via `ffigen` for native libraries. Wrap allocations in a
  `Finalizable` to release on garbage collection.

### Testing

- Unit tests for pure Dart, widget tests for UI behavior, golden tests for
  pixel critical screens, integration tests for one or two end to end
  smoke flows.
- Run goldens only on a fixed platform in CI (Linux is conventional)
  because rendering differs across hosts.

### CI / CD

- GitHub Actions or Codemagic. Steps: `flutter analyze`, `dart format
  --set-exit-if-changed`, `flutter test --coverage`, golden update gate,
  build per flavor, fastlane to TestFlight and Play internal testing.

## Deliverables

### Widget skeleton with const and proper keys

```dart
class TaskTile extends StatelessWidget {
  const TaskTile({super.key, required this.task, required this.onToggle});

  final Task task;
  final ValueChanged<bool> onToggle;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return ListTile(
      leading: Checkbox(value: task.done, onChanged: (v) => onToggle(v ?? false)),
      title: Text(task.title, style: theme.textTheme.bodyLarge),
      subtitle: task.due != null
          ? Text(task.due!.toIso8601String(), style: theme.textTheme.bodySmall)
          : null,
    );
  }
}
```

### Riverpod notifier provider plus consumer

```dart
@riverpod
class TaskList extends _$TaskList {
  @override
  Future<List<Task>> build() async {
    return ref.read(taskRepoProvider).fetchAll();
  }

  Future<void> toggle(String id, bool done) async {
    final repo = ref.read(taskRepoProvider);
    state = AsyncData([
      for (final t in state.value ?? const <Task>[])
        if (t.id == id) t.copyWith(done: done) else t,
    ]);
    try {
      await repo.setDone(id, done);
    } catch (e, st) {
      state = AsyncError(e, st);
    }
  }
}

class TasksScreen extends ConsumerWidget {
  const TasksScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final tasks = ref.watch(taskListProvider);
    return tasks.when(
      data: (items) => ListView.builder(
        itemCount: items.length,
        itemBuilder: (_, i) => TaskTile(
          key: ValueKey(items[i].id),
          task: items[i],
          onToggle: (v) => ref.read(taskListProvider.notifier).toggle(items[i].id, v),
        ),
      ),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (e, _) => Center(child: Text('Failed: $e')),
    );
  }
}
```

### go_router with typed routes

```dart
final appRouter = GoRouter(
  initialLocation: '/',
  redirect: (ctx, state) {
    final loggedIn = ctx.read<AuthService>().isAuthenticated;
    final goingToLogin = state.matchedLocation == '/login';
    if (!loggedIn && !goingToLogin) return '/login';
    if (loggedIn && goingToLogin) return '/';
    return null;
  },
  routes: [
    GoRoute(path: '/', builder: (_, __) => const TasksScreen()),
    GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
    GoRoute(
      path: '/task/:id',
      builder: (_, s) => TaskDetailScreen(id: s.pathParameters['id']!),
    ),
  ],
);
```

### Platform channel (Dart plus iOS Swift plus Android Kotlin)

```dart
class BatteryChannel {
  static const _channel = MethodChannel('dev.example.app/battery');

  Future<int> levelPercent() async {
    try {
      final v = await _channel.invokeMethod<int>('getBatteryLevel');
      return v ?? -1;
    } on PlatformException catch (e) {
      throw BatteryException(code: e.code, message: e.message);
    }
  }
}
```

```swift
// ios/Runner/BatteryPlugin.swift
import Flutter
import UIKit

final class BatteryPlugin: NSObject, FlutterPlugin {
  static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(
      name: "dev.example.app/battery", binaryMessenger: registrar.messenger())
    registrar.addMethodCallDelegate(BatteryPlugin(), channel: channel)
  }
  func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    guard call.method == "getBatteryLevel" else { result(FlutterMethodNotImplemented); return }
    UIDevice.current.isBatteryMonitoringEnabled = true
    let level = Int(UIDevice.current.batteryLevel * 100)
    if level < 0 { result(FlutterError(code: "UNAVAILABLE", message: "No battery info", details: nil)) }
    else { result(level) }
  }
}
```

```kotlin
// android/app/src/main/kotlin/.../BatteryPlugin.kt
class BatteryPlugin : FlutterPlugin, MethodCallHandler {
  private lateinit var channel: MethodChannel
  override fun onAttachedToEngine(b: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(b.binaryMessenger, "dev.example.app/battery")
    channel.setMethodCallHandler(this)
  }
  override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
    if (call.method != "getBatteryLevel") { result.notImplemented(); return }
    val mgr = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
    val pct = mgr.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    if (pct < 0) result.error("UNAVAILABLE", "No battery info", null) else result.success(pct)
  }
  override fun onDetachedFromEngine(b: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }
}
```

### Golden test setup

```dart
void main() {
  testGoldens('TaskTile renders done and pending', (tester) async {
    final builder = DeviceBuilder()
      ..addScenario(widget: const TaskTile(task: Task.pending), name: 'pending')
      ..addScenario(widget: const TaskTile(task: Task.done), name: 'done');
    await tester.pumpDeviceBuilder(builder);
    await screenMatchesGolden(tester, 'task_tile');
  });
}
// CI policy: never run with --update-goldens on main. Update in a PR with
// screenshots attached and a reviewer who has compared the diffs.
```

### pubspec.yaml with pinned versions

```yaml
name: example_app
description: Example Flutter app.
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.4.0 <4.0.0'
  flutter: '>=3.24.0'

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: 2.5.1
  riverpod_annotation: 2.3.5
  go_router: 14.2.0
  dio: 5.5.0
  drift: 2.18.0
  freezed_annotation: 2.4.4
  json_annotation: 4.9.0
  flutter_secure_storage: 9.2.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter
  build_runner: 2.4.11
  riverpod_generator: 2.4.2
  freezed: 2.5.7
  json_serializable: 6.8.0
  flutter_lints: 4.0.0
  golden_toolkit: 0.15.0
```

## Quality bar

- `flutter analyze` passes with zero warnings; `dart format` is clean.
- Every widget that can be `const`, is `const`. No `Colors.X` or magic
  numbers in widget files; values come from `Theme.of(context)` or tokens.
- One state management library is used app wide; no rogue
  `setState` calls that should be a provider.
- Every `StreamSubscription`, `Timer`, and `AnimationController` is
  cancelled or disposed in `dispose`.
- Heavy work runs in an isolate via `compute` or a long lived isolate;
  the UI thread frame budget stays under 16.6 ms on a midrange device.
- Platform channels live behind a Dart interface; the app never imports
  `MethodChannel` outside the channel package.
- Golden tests cover critical UI; goldens are only updated in PRs with
  screenshots and reviewer sign off.
- l10n is wired from day one; no hardcoded user facing strings.
- Build flavors are configured; secrets do not live in the bundle.

## Antipatterns

- Rebuilding entire trees on every state change because nothing is `const`,
  no `Selector` or Riverpod `select` is used, and the top widget owns all
  state.
- State lived in a `StatefulWidget` that should live in a provider or Bloc
  so multiple screens can read it without prop drilling.
- Async work started in `initState` without cancellation in `dispose`,
  leaking memory and writing to a disposed widget.
- Inline colors, paddings, and strings. No theme, no l10n, no tokens.
- `setState` deep in a tree when a Riverpod or Bloc provider would scope
  the rebuild to one widget.
- Missing keys on reorderable list items, breaking implicit animations and
  state preservation across reorders.
- Shipping without golden tests on critical UI, then breaking the home
  screen on a refactor with no signal.
- Mixing Provider, Bloc, Riverpod, and GetX in one app because every
  contributor reached for their favorite.
- Deeply nested `Future.then` chains instead of `async`/`await`.
- Blocking the UI thread with JSON parsing, image decoding, or crypto.
  Move it to `compute` or a long lived isolate.
- Putting secrets in `pubspec.yaml` or asset files. Use a backend or
  flavored runtime configuration.

## Handoffs

- Cross platform framework selection across native, React Native, Flutter,
  and KMP: `senior-mobile-engineer`.
- iOS specific platform channel work, Notification Service Extensions,
  Swift concurrency, App Store policy nuance: `swift-ios-expert`.
- Shared design system thinking with a web sibling and component reuse:
  `senior-frontend-engineer`.
- Cross platform UX, Material 3 versus Cupertino call, accessibility audit:
  `senior-ux-designer`.
- API surface the app consumes, endpoint shapes, pagination, error
  contracts: `senior-backend-engineer`, `api-contract-designer`.
- Frame timing diagnosis, isolate strategy for CPU heavy work, memory
  pressure investigation: `senior-performance-engineer`.
- Release pipelines, fastlane signing automation, crash analytics
  configuration: `senior-devops-sre`.
- Keystore and Keychain policy, certificate pinning, threat model:
  `principal-security-engineer`.
- Test plans across unit, widget, golden, integration, device farm:
  `senior-qa-test-engineer`.

## Quick reference

| Question | Answer |
|---|---|
| What does this skill produce? | Widget skeletons, Riverpod notifier providers, go_router configs, platform channel scaffolds, golden test setups, pinned pubspec.yaml. |
| What does it not do? | Cross platform framework selection; iOS or Android dialect deep dives; backend API design. |
| Default state management | Riverpod with generators. Bloc when the team prefers; Provider for tiny surfaces; signals for fine grained reactivity. |
| Default navigation | `go_router` with typed routes and one auth redirect. |
| Default persistence | `drift` for relational, `isar` for object store, `flutter_secure_storage` for tokens. |
| Default rendering target | Impeller on iOS, Skia on Android. Profile on both, real midrange device. |
| Frame budget | 16.6 ms at 60 fps, 8.3 ms at 120 fps. Absolute. |
| Default test stance | Unit plus widget plus golden on critical UI; integration sparingly; goldens updated only in PRs. |
| Common partner skills | `senior-mobile-engineer`, `swift-ios-expert`, `senior-frontend-engineer`, `senior-ux-designer`, `senior-backend-engineer`, `api-contract-designer`, `senior-performance-engineer`, `senior-devops-sre`, `principal-security-engineer`, `senior-qa-test-engineer`. |

