Flutter Skill
When to use
- Creating or modifying Flutter screens, widgets, or navigation flows
- Choosing a state management approach (Riverpod, Bloc, Provider, GetX)
- Integrating platform channels for native iOS/Android APIs
- Configuring
pubspec.yaml, flavors, or build variants - Diagnosing jank, excessive rebuilds, or memory leaks
- Setting up CI builds for iOS, Android, web, or desktop targets
Workflow
- Confirm Flutter channel and target platforms — check
flutter --versionand which platforms are enabled inpubspec.yaml. Use stable channel for production; beta only if a specific fix is needed. - Plan widget tree before coding:
- Identify which parts of the UI are stateless (pure display) vs. stateful (user interaction, async data)
- Keep
StatelessWidgetthe default; only useStatefulWidgetwhen local ephemeral state is genuinely needed - Prefer
constconstructors everywhere possible — they short-circuit the rebuild cycle
- Choose state management (confirm with team; do not switch mid-project):
- Riverpod (recommended default):
AsyncNotifierProviderfor async data,NotifierProviderfor sync state; co-locate provider declarations near the feature - Bloc:
Cubitfor simple state,Blocfor event-driven flows; one Bloc per feature/screen - Provider: acceptable for smaller apps; avoid nested
ChangeNotifierProviderchains
- Riverpod (recommended default):
- Navigation: use
go_routerfor declarative, deep-link-aware routing. Define all routes in a singlerouter.dart; avoidNavigator.of(context).pushcalls scattered through the widget tree. - Data layer:
- HTTP:
dioorhttppackage; wrap in a typed repository class - Local storage:
drift(SQL) for structured data,hive/isarfor simple key-value/object stores - Never call network or DB code inside
build()
- HTTP:
- Async in widgets: use
FutureBuilder/StreamBuilderonly for one-off display; for anything the user can refresh or that changes over time, use a state management provider that holds the async state. - Platform channels: define the method channel in a dedicated service class; keep channel names namespaced (
com.example.app/channelName); implement both iOS (Swift) and Android (Kotlin) sides before writing the Dart wrapper. - Performance pass before each release:
- Run
flutter run --profileon a real device; open DevTools → Performance - Identify widgets rebuilding unnecessarily with the Widget Rebuild tracker
- Use
RepaintBoundaryaround independently animated sections - Ensure all
Imagewidgets usecacheWidth/cacheHeightorResizeImage
- Run
- Build and sign:
- iOS: manage certificates and provisioning in Xcode or Fastlane; never commit
.p12orExportOptions.plistwith embedded secrets - Android: store keystore outside the repo; reference via
key.properties(git-ignored) - Use flavors (
--flavor) for dev/staging/prod; map to separate Firebase projects if applicable
- iOS: manage certificates and provisioning in Xcode or Fastlane; never commit
Standards
| Area | Do | Do not |
|---|---|---|
| Widgets | Prefer const constructors; extract repeated sub-trees into named widgets |
Build deeply nested anonymous closures inside build() |
| State | Hold state in providers/blocs; pass only what a widget needs via constructor | Store app state in BuildContext extensions or global variables |
| Keys | Use ValueKey / ObjectKey on list items; GlobalKey only when required (form, navigator) |
Assign GlobalKey to every widget "just in case" |
| Async | Handle loading, error, and empty states explicitly | Use .then() chains inside build(); ignore AsyncError |
| Theming | Define all colors, text styles, and spacing in ThemeData; reference via Theme.of(context) |
Hardcode hex colors or font sizes in widget files |
| Testing | Unit-test all business logic; widget-test critical flows; integration-test happy paths on CI | Skip tests because "Flutter UI is hard to test" |
| Secrets | Load API keys from --dart-define or a git-ignored .env read at build time |
Embed secrets in lib/ source files |
Common mistakes to avoid
setStateon a disposed widget — always checkmountedbefore callingsetStateafter anawait.- Rebuilding expensive widgets on every parent rebuild — lift the expensive widget out, wrap in
const, or useSelector/Consumerscoped to the exact state slice it needs. BuildContextacross async gaps — after anyawait, the context may be invalid. Store what you need before the await, or checkmounted.pubspec.yamlasset paths with trailing slash not listing all files — Flutter does not glob recursively; each subfolder must be listed, or files will be missing in release builds.- Platform channel calls on the main isolate for heavy work — offload to a background
Isolateor usecompute(); blocking the main isolate causes dropped frames. - Not handling deep links in go_router on both platforms — iOS requires
CFBundleURLTypesinInfo.plist; Android requires intent filters inAndroidManifest.xml. Test both. - Forgetting
flutter pub run build_runner buildafter model changes — generated files (*.g.dart,*.freezed.dart) go stale silently.
Output format
Typical feature deliverable structure:
lib/
features/
<feature>/
data/
<feature>_repository.dart # Network/DB calls; returns typed models
<feature>_model.dart # Freezed or plain Dart model + fromJson/toJson
domain/
<feature>_state.dart # State class (Freezed union or sealed class)
presentation/
<feature>_screen.dart # Root screen widget; provides BLoC/Riverpod scope
widgets/
<feature>_card.dart # Extracted sub-widgets; prefer const
core/
router/
router.dart # All go_router route definitions
theme/
app_theme.dart
test/
features/
<feature>/
<feature>_repository_test.dart
<feature>_screen_test.dart
Related checklists
.claude/checklists/security.md.claude/checklists/performance.md.claude/checklists/accessibility.md.claude/checklists/production.md
Related agents
.claude/agents/engineering/mobile-engineer.md.claude/agents/design/mobile-ux-specialist.md.claude/agents/quality/performance-engineer.md.claude/agents/quality/security-auditor.md.claude/agents/quality/accessibility-auditor.md