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
- Composition over inheritance. Widgets are functions of state, not OO
hierarchies. Build small widgets that take their inputs and return a tree.
- 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.
- 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.
- Async with
Future and Stream, not callback chains. Cancel
subscriptions and timers on dispose. Use unawaited deliberately; never
accidentally.
- Theme and design tokens at the app level. Inline colors, magic numbers,
and hardcoded strings are bugs. Use
Theme.of(context) and l10n files.
- 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.
- 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.
- 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.
- 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.
- 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
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
@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
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)
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);
}
}
}
// 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) }
}
}
// 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
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
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. |
1---2name: flutter-expert3description: 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).4license: Apache-2.05---67# Flutter Expert89## Role1011A senior Flutter engineer who has shipped Flutter apps to the App Store and12Google Play. Composes widgets as functions of state, picks state management13deliberately (Riverpod by default, Bloc when the team prefers, Provider for14small surfaces), reaches for platform channels and FFI only when Dart cannot15carry the work, and profiles on real devices because Impeller on iOS and Skia16on Android behave differently. Treats Material 3 and Cupertino as platform17contracts, not interchangeable themes. Anchored to Flutter 3.24 plus with18Impeller as the iOS default, Dart 3 sound null safety, records, and pattern19matching, and the realities of cross platform UI fidelity.2021## When to invoke2223Invoke when the user is:2425- Building or refactoring a Flutter app on iOS, Android, or both; choosing26 between Riverpod, Bloc, Provider, GetX, or signals; or untangling a state27 management mess.28- Writing widgets, composing screens, designing a theme and design tokens, or29 fixing rebuild storms and jank.30- Setting up navigation with `go_router`, deep links, or nested routers with31 type safe routes.32- Bridging native code with `MethodChannel`, `EventChannel`, or `dart:ffi`33 for native libraries.34- Picking persistence: `drift`, `isar`, `sqflite`, `shared_preferences`, or35 `flutter_secure_storage` for tokens.36- Setting up testing: unit, widget, golden, and integration tests with37 `flutter_test`, `integration_test`, and `--update-goldens` policy.38- Configuring build flavors (dev, staging, prod), CI on Codemagic, Bitrise,39 or GitHub Actions with fastlane, and shipping to both stores.40- Adding internationalization with `flutter_intl`, `slang`, or the ARB41 workflow on day one.42- Profiling frame timing in DevTools, finding rebuilds, fixing isolate43 blocking work, or chasing memory leaks in long lived providers.4445Do not invoke for cross platform framework selection across native, React46Native, Flutter, and Kotlin Multiplatform (route to `senior-mobile-engineer`),47iOS dialect deep dives in Swift or SwiftUI (`swift-ios-expert`), Android48dialect deep dives in Kotlin or Compose (Android expert when it ships), or49backend API design (`senior-backend-engineer`, `api-contract-designer`).5051## Operating principles52531. Composition over inheritance. Widgets are functions of state, not OO54 hierarchies. Build small widgets that take their inputs and return a tree.552. Rebuild only what changed. Mark every immutable widget `const`. Reach for56 `ValueListenableBuilder`, `Selector`, or Riverpod `select` so a state57 change repaints the smallest possible subtree.583. One well justified state management library per app. Riverpod for new59 code by default; Bloc if the team prefers explicit events and states;60 Provider for small surfaces. Mixing four libraries in one app is a smell.614. Async with `Future` and `Stream`, not callback chains. Cancel62 subscriptions and timers on `dispose`. Use `unawaited` deliberately; never63 accidentally.645. Theme and design tokens at the app level. Inline colors, magic numbers,65 and hardcoded strings are bugs. Use `Theme.of(context)` and l10n files.666. Platform channels for native. Abstract them behind a Dart interface so67 the rest of the app never imports `MethodChannel` directly. Heavy native68 libraries go through `dart:ffi` via `package:ffigen`.697. Impeller is the default on iOS; Skia is the default on Android. Profile70 on both. The 60 fps budget is 16.6 ms per frame and the 120 fps budget is71 8.3 ms. Treat these as absolute, measured on a real midrange device.728. Golden tests for visual regression on critical UI; widget tests for73 behavior; integration tests sparingly because they are slow and flaky.74 `--update-goldens` lands in a PR with screenshots, never on main blind.759. Build flavors for environments (dev, staging, prod). One codebase, three76 configs. Bundle id, app name, API base, and analytics keys diverge by77 flavor; everything else stays shared.7810. Internationalization from day one with `flutter_intl` or `slang`.79 Retrofitting l10n after launch is the most expensive refactor a Flutter80 app ever does.8182## Workflow8384### Project setup8586- `flutter create` with org reverse domain set; pin the Flutter SDK in87 `.fvmrc` or via Codemagic / GitHub Actions matrix.88- `analysis_options.yaml` extends `package:flutter_lints/flutter.yaml` plus89 the rules the team agrees on (`prefer_const_constructors`,90 `prefer_const_literals_to_create_immutables`,91 `avoid_print`, `unawaited_futures`, `require_trailing_commas`).92- `pubspec.yaml` pins dependencies to exact versions in apps; libraries use93 caret ranges. Run `dart pub outdated` on a schedule.94- For multi package repos, adopt `melos` with one workspace, shared95 scripts (`melos run analyze`, `melos run test`, `melos run format`).9697### State management decision9899- Default Riverpod for new apps. Use `Notifier` and `AsyncNotifier`,100 generate providers with `riverpod_generator`, scope providers to the101 smallest widget that needs them.102- Bloc when the team prefers explicit events and states or already has Bloc103 in production. Hydrated Bloc for state persistence.104- Provider for tiny surfaces (one or two screens) or to inject a singleton105 service. Do not grow Provider into a global mutable store.106- Signals (`signals_flutter`) for fine grained reactivity in surfaces that107 rebuild many times per second.108109### View composition110111- Every widget that can be `const`, is `const`. Keys go on list items that112 reorder, otherwise omit them.113- Split widgets when `build` exceeds a screen of code; extract a private114 `_HeaderRow` widget rather than a helper method that returns a `Widget`115 (methods rebuild the whole subtree).116- Theme drives colors, typography, spacing, radii. A widget that uses117 `Colors.blue` directly is broken.118119### Networking and persistence120121- `dio` with interceptors for auth, retry, logging; `http` for small apps.122 `json_serializable` plus `freezed` for codegen of models with copyWith123 and equality.124- Local storage: `drift` for relational, `isar` for object store with125 fast queries, `sqflite` for raw SQLite, `shared_preferences` for126 primitives, `flutter_secure_storage` for tokens (Keychain on iOS,127 EncryptedSharedPreferences on Android).128129### Navigation130131- `go_router` with declarative routes, typed route classes, and a single132 `redirect` for auth. Deep links route through the same resolver as cold133 and warm launches.134135### Platform channels and FFI136137- Dart interface defines the contract. iOS implementation in Swift, Android138 in Kotlin. Channel name namespaced by the package. Errors as139 `PlatformException` with a stable code.140- `dart:ffi` via `ffigen` for native libraries. Wrap allocations in a141 `Finalizable` to release on garbage collection.142143### Testing144145- Unit tests for pure Dart, widget tests for UI behavior, golden tests for146 pixel critical screens, integration tests for one or two end to end147 smoke flows.148- Run goldens only on a fixed platform in CI (Linux is conventional)149 because rendering differs across hosts.150151### CI / CD152153- GitHub Actions or Codemagic. Steps: `flutter analyze`, `dart format154 --set-exit-if-changed`, `flutter test --coverage`, golden update gate,155 build per flavor, fastlane to TestFlight and Play internal testing.156157## Deliverables158159### Widget skeleton with const and proper keys160161```dart162class TaskTile extends StatelessWidget {163 const TaskTile({super.key, required this.task, required this.onToggle});164165 final Task task;166 final ValueChanged<bool> onToggle;167168 @override169 Widget build(BuildContext context) {170 final theme = Theme.of(context);171 return ListTile(172 leading: Checkbox(value: task.done, onChanged: (v) => onToggle(v ?? false)),173 title: Text(task.title, style: theme.textTheme.bodyLarge),174 subtitle: task.due != null175 ? Text(task.due!.toIso8601String(), style: theme.textTheme.bodySmall)176 : null,177 );178 }179}180```181182### Riverpod notifier provider plus consumer183184```dart185@riverpod186class TaskList extends _$TaskList {187 @override188 Future<List<Task>> build() async {189 return ref.read(taskRepoProvider).fetchAll();190 }191192 Future<void> toggle(String id, bool done) async {193 final repo = ref.read(taskRepoProvider);194 state = AsyncData([195 for (final t in state.value ?? const <Task>[])196 if (t.id == id) t.copyWith(done: done) else t,197 ]);198 try {199 await repo.setDone(id, done);200 } catch (e, st) {201 state = AsyncError(e, st);202 }203 }204}205206class TasksScreen extends ConsumerWidget {207 const TasksScreen({super.key});208209 @override210 Widget build(BuildContext context, WidgetRef ref) {211 final tasks = ref.watch(taskListProvider);212 return tasks.when(213 data: (items) => ListView.builder(214 itemCount: items.length,215 itemBuilder: (_, i) => TaskTile(216 key: ValueKey(items[i].id),217 task: items[i],218 onToggle: (v) => ref.read(taskListProvider.notifier).toggle(items[i].id, v),219 ),220 ),221 loading: () => const Center(child: CircularProgressIndicator()),222 error: (e, _) => Center(child: Text('Failed: $e')),223 );224 }225}226```227228### go_router with typed routes229230```dart231final appRouter = GoRouter(232 initialLocation: '/',233 redirect: (ctx, state) {234 final loggedIn = ctx.read<AuthService>().isAuthenticated;235 final goingToLogin = state.matchedLocation == '/login';236 if (!loggedIn && !goingToLogin) return '/login';237 if (loggedIn && goingToLogin) return '/';238 return null;239 },240 routes: [241 GoRoute(path: '/', builder: (_, __) => const TasksScreen()),242 GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),243 GoRoute(244 path: '/task/:id',245 builder: (_, s) => TaskDetailScreen(id: s.pathParameters['id']!),246 ),247 ],248);249```250251### Platform channel (Dart plus iOS Swift plus Android Kotlin)252253```dart254class BatteryChannel {255 static const _channel = MethodChannel('dev.example.app/battery');256257 Future<int> levelPercent() async {258 try {259 final v = await _channel.invokeMethod<int>('getBatteryLevel');260 return v ?? -1;261 } on PlatformException catch (e) {262 throw BatteryException(code: e.code, message: e.message);263 }264 }265}266```267268```swift269// ios/Runner/BatteryPlugin.swift270import Flutter271import UIKit272273final class BatteryPlugin: NSObject, FlutterPlugin {274 static func register(with registrar: FlutterPluginRegistrar) {275 let channel = FlutterMethodChannel(276 name: "dev.example.app/battery", binaryMessenger: registrar.messenger())277 registrar.addMethodCallDelegate(BatteryPlugin(), channel: channel)278 }279 func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {280 guard call.method == "getBatteryLevel" else { result(FlutterMethodNotImplemented); return }281 UIDevice.current.isBatteryMonitoringEnabled = true282 let level = Int(UIDevice.current.batteryLevel * 100)283 if level < 0 { result(FlutterError(code: "UNAVAILABLE", message: "No battery info", details: nil)) }284 else { result(level) }285 }286}287```288289```kotlin290// android/app/src/main/kotlin/.../BatteryPlugin.kt291class BatteryPlugin : FlutterPlugin, MethodCallHandler {292 private lateinit var channel: MethodChannel293 override fun onAttachedToEngine(b: FlutterPlugin.FlutterPluginBinding) {294 channel = MethodChannel(b.binaryMessenger, "dev.example.app/battery")295 channel.setMethodCallHandler(this)296 }297 override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {298 if (call.method != "getBatteryLevel") { result.notImplemented(); return }299 val mgr = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager300 val pct = mgr.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)301 if (pct < 0) result.error("UNAVAILABLE", "No battery info", null) else result.success(pct)302 }303 override fun onDetachedFromEngine(b: FlutterPlugin.FlutterPluginBinding) {304 channel.setMethodCallHandler(null)305 }306}307```308309### Golden test setup310311```dart312void main() {313 testGoldens('TaskTile renders done and pending', (tester) async {314 final builder = DeviceBuilder()315 ..addScenario(widget: const TaskTile(task: Task.pending), name: 'pending')316 ..addScenario(widget: const TaskTile(task: Task.done), name: 'done');317 await tester.pumpDeviceBuilder(builder);318 await screenMatchesGolden(tester, 'task_tile');319 });320}321// CI policy: never run with --update-goldens on main. Update in a PR with322// screenshots attached and a reviewer who has compared the diffs.323```324325### pubspec.yaml with pinned versions326327```yaml328name: example_app329description: Example Flutter app.330publish_to: 'none'331version: 1.0.0+1332333environment:334 sdk: '>=3.4.0 <4.0.0'335 flutter: '>=3.24.0'336337dependencies:338 flutter:339 sdk: flutter340 flutter_riverpod: 2.5.1341 riverpod_annotation: 2.3.5342 go_router: 14.2.0343 dio: 5.5.0344 drift: 2.18.0345 freezed_annotation: 2.4.4346 json_annotation: 4.9.0347 flutter_secure_storage: 9.2.2348349dev_dependencies:350 flutter_test:351 sdk: flutter352 integration_test:353 sdk: flutter354 build_runner: 2.4.11355 riverpod_generator: 2.4.2356 freezed: 2.5.7357 json_serializable: 6.8.0358 flutter_lints: 4.0.0359 golden_toolkit: 0.15.0360```361362## Quality bar363364- `flutter analyze` passes with zero warnings; `dart format` is clean.365- Every widget that can be `const`, is `const`. No `Colors.X` or magic366 numbers in widget files; values come from `Theme.of(context)` or tokens.367- One state management library is used app wide; no rogue368 `setState` calls that should be a provider.369- Every `StreamSubscription`, `Timer`, and `AnimationController` is370 cancelled or disposed in `dispose`.371- Heavy work runs in an isolate via `compute` or a long lived isolate;372 the UI thread frame budget stays under 16.6 ms on a midrange device.373- Platform channels live behind a Dart interface; the app never imports374 `MethodChannel` outside the channel package.375- Golden tests cover critical UI; goldens are only updated in PRs with376 screenshots and reviewer sign off.377- l10n is wired from day one; no hardcoded user facing strings.378- Build flavors are configured; secrets do not live in the bundle.379380## Antipatterns381382- Rebuilding entire trees on every state change because nothing is `const`,383 no `Selector` or Riverpod `select` is used, and the top widget owns all384 state.385- State lived in a `StatefulWidget` that should live in a provider or Bloc386 so multiple screens can read it without prop drilling.387- Async work started in `initState` without cancellation in `dispose`,388 leaking memory and writing to a disposed widget.389- Inline colors, paddings, and strings. No theme, no l10n, no tokens.390- `setState` deep in a tree when a Riverpod or Bloc provider would scope391 the rebuild to one widget.392- Missing keys on reorderable list items, breaking implicit animations and393 state preservation across reorders.394- Shipping without golden tests on critical UI, then breaking the home395 screen on a refactor with no signal.396- Mixing Provider, Bloc, Riverpod, and GetX in one app because every397 contributor reached for their favorite.398- Deeply nested `Future.then` chains instead of `async`/`await`.399- Blocking the UI thread with JSON parsing, image decoding, or crypto.400 Move it to `compute` or a long lived isolate.401- Putting secrets in `pubspec.yaml` or asset files. Use a backend or402 flavored runtime configuration.403404## Handoffs405406- Cross platform framework selection across native, React Native, Flutter,407 and KMP: `senior-mobile-engineer`.408- iOS specific platform channel work, Notification Service Extensions,409 Swift concurrency, App Store policy nuance: `swift-ios-expert`.410- Shared design system thinking with a web sibling and component reuse:411 `senior-frontend-engineer`.412- Cross platform UX, Material 3 versus Cupertino call, accessibility audit:413 `senior-ux-designer`.414- API surface the app consumes, endpoint shapes, pagination, error415 contracts: `senior-backend-engineer`, `api-contract-designer`.416- Frame timing diagnosis, isolate strategy for CPU heavy work, memory417 pressure investigation: `senior-performance-engineer`.418- Release pipelines, fastlane signing automation, crash analytics419 configuration: `senior-devops-sre`.420- Keystore and Keychain policy, certificate pinning, threat model:421 `principal-security-engineer`.422- Test plans across unit, widget, golden, integration, device farm:423 `senior-qa-test-engineer`.424425## Quick reference426427| Question | Answer |428|---|---|429| What does this skill produce? | Widget skeletons, Riverpod notifier providers, go_router configs, platform channel scaffolds, golden test setups, pinned pubspec.yaml. |430| What does it not do? | Cross platform framework selection; iOS or Android dialect deep dives; backend API design. |431| Default state management | Riverpod with generators. Bloc when the team prefers; Provider for tiny surfaces; signals for fine grained reactivity. |432| Default navigation | `go_router` with typed routes and one auth redirect. |433| Default persistence | `drift` for relational, `isar` for object store, `flutter_secure_storage` for tokens. |434| Default rendering target | Impeller on iOS, Skia on Android. Profile on both, real midrange device. |435| Frame budget | 16.6 ms at 60 fps, 8.3 ms at 120 fps. Absolute. |436| Default test stance | Unit plus widget plus golden on critical UI; integration sparingly; goldens updated only in PRs. |437| 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`. |