Everything-claude-code flutter-dart-code-review
Library-agnostic Flutter/Dart code review checklist covering widget best practices, state management patterns (BLoC, Riverpod, Provider, GetX, MobX, Signals), Dart idioms, performance, accessibility, security, and clean architecture.
install
source · Clone the upstream repo
git clone https://github.com/affaan-m/everything-claude-code
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/affaan-m/everything-claude-code "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/flutter-dart-code-review" ~/.claude/skills/affaan-m-everything-claude-code-flutter-dart-code-review-908b32 && rm -rf "$T"
manifest:
skills/flutter-dart-code-review/SKILL.mdsource content
Flutter/Dart Code Review Best Practices
Comprehensive, library-agnostic checklist for reviewing Flutter/Dart applications. These principles apply regardless of which state management solution, routing library, or DI framework is used.
1. General Project Health
- Project follows consistent folder structure (feature-first or layer-first)
- Proper separation of concerns: UI, business logic, data layers
- No business logic in widgets; widgets are purely presentational
-
is clean — no unused dependencies, versions pinned appropriatelypubspec.yaml -
includes a strict lint set with strict analyzer settings enabledanalysis_options.yaml - No
statements in production code — useprint()dart:developer
or a logging packagelog() - Generated files (
,.g.dart
,.freezed.dart
) are up-to-date or in.gr.dart.gitignore - Platform-specific code isolated behind abstractions
2. Dart Language Pitfalls
- Implicit dynamic: Missing type annotations leading to
— enabledynamic
,strict-casts
,strict-inferencestrict-raw-types - Null safety misuse: Excessive
(bang operator) instead of proper null checks or Dart 3 pattern matching (!
)if (value case var v?) - Type promotion failures: Using
where local variable promotion would workthis.field - Catching too broadly:
withoutcatch (e)
clause; always specify exception typeson - Catching
:Error
subtypes indicate bugs and should not be caughtError - Unused
: Functions markedasync
that neverasync
— unnecessary overheadawait -
overuse:late
used where nullable or constructor initialization would be safer; defers errors to runtimelate - String concatenation in loops: Use
instead ofStringBuffer
for iterative string building+ - Mutable state in
contexts: Fields inconst
constructor classes should not be mutableconst - Ignoring
return values: UseFuture
or explicitly callawait
to signal intentunawaited() -
wherevar
works: Preferfinal
for locals andfinal
for compile-time constantsconst - Relative imports: Use
imports for consistencypackage: - Mutable collections exposed: Public APIs should return unmodifiable views, not raw
/ListMap - Missing Dart 3 pattern matching: Prefer switch expressions and
over verboseif-case
checks and manual castingis - Throwaway classes for multiple returns: Use Dart 3 records
instead of single-use DTOs(String, int) -
in production code: Useprint()dart:developer
or the project's logging package;log()
has no log levels and cannot be filteredprint()
3. Widget Best Practices
Widget decomposition:
- No single widget with a
method exceeding ~80-100 linesbuild() - Widgets split by encapsulation AND by how they change (rebuild boundaries)
- Private
helper methods that return widgets are extracted to separate widget classes (enables element reuse, const propagation, and framework optimizations)_build*() - Stateless widgets preferred over Stateful where no mutable local state is needed
- Extracted widgets are in separate files when reusable
Const usage:
-
constructors used wherever possible — prevents unnecessary rebuildsconst -
literals for collections that don't change (const
,const []
)const {} - Constructor is declared
when all fields are finalconst
Key usage:
-
used in lists/grids to preserve state across reordersValueKey -
used sparingly — only when accessing state across the tree is truly neededGlobalKey -
avoided inUniqueKey
— it forces rebuild every framebuild() -
used when identity is based on a data object rather than a single valueObjectKey
Theming & design system:
- Colors come from
— no hardcodedTheme.of(context).colorScheme
or hex valuesColors.red - Text styles come from
— no inlineTheme.of(context).textTheme
with raw font sizesTextStyle - Dark mode compatibility verified — no assumptions about light background
- Spacing and sizing use consistent design tokens or constants, not magic numbers
Build method complexity:
- No network calls, file I/O, or heavy computation in
build() - No
orFuture.then()
work inasyncbuild() - No subscription creation (
) in.listen()build() -
localized to smallest possible subtreesetState()
4. State Management (Library-Agnostic)
These principles apply to all Flutter state management solutions (BLoC, Riverpod, Provider, GetX, MobX, Signals, ValueNotifier, etc.).
Architecture:
- Business logic lives outside the widget layer — in a state management component (BLoC, Notifier, Controller, Store, ViewModel, etc.)
- State managers receive dependencies via injection, not by constructing them internally
- A service or repository layer abstracts data sources — widgets and state managers should not call APIs or databases directly
- State managers have a single responsibility — no "god" managers handling unrelated concerns
- Cross-component dependencies follow the solution's conventions:
- In Riverpod: providers depending on providers via
is expected — flag only circular or overly tangled chainsref.watch - In BLoC: blocs should not directly depend on other blocs — prefer shared repositories or presentation-layer coordination
- In other solutions: follow the documented conventions for inter-component communication
- In Riverpod: providers depending on providers via
Immutability & value equality (for immutable-state solutions: BLoC, Riverpod, Redux):
- State objects are immutable — new instances created via
or constructors, never mutated in-placecopyWith() - State classes implement
and==
properly (all fields included in comparison)hashCode - Mechanism is consistent across the project — manual override,
,Equatable
, Dart records, or otherfreezed - Collections inside state objects are not exposed as raw mutable
/ListMap
Reactivity discipline (for reactive-mutation solutions: MobX, GetX, Signals):
- State is only mutated through the solution's reactive API (
in MobX,@action
on signals,.value
in GetX) — direct field mutation bypasses change tracking.obs - Derived values use the solution's computed mechanism rather than being stored redundantly
- Reactions and disposers are properly cleaned up (
in MobX, effect cleanup in Signals)ReactionDisposer
State shape design:
- Mutually exclusive states use sealed types, union variants, or the solution's built-in async state type (e.g. Riverpod's
) — not boolean flags (AsyncValue
,isLoading
,isError
)hasData - Every async operation models loading, success, and error as distinct states
- All state variants are handled exhaustively in UI — no silently ignored cases
- Error states carry error information for display; loading states don't carry stale data
- Nullable data is not used as a loading indicator — states are explicit
// BAD — boolean flag soup allows impossible states class UserState { bool isLoading = false; bool hasError = false; // isLoading && hasError is representable! User? user; } // GOOD (immutable approach) — sealed types make impossible states unrepresentable sealed class UserState {} class UserInitial extends UserState {} class UserLoading extends UserState {} class UserLoaded extends UserState { final User user; const UserLoaded(this.user); } class UserError extends UserState { final String message; const UserError(this.message); } // GOOD (reactive approach) — observable enum + data, mutations via reactivity API // enum UserStatus { initial, loading, loaded, error } // Use your solution's observable/signal to wrap status and data separately
Rebuild optimization:
- State consumer widgets (Builder, Consumer, Observer, Obx, Watch, etc.) scoped as narrow as possible
- Selectors used to rebuild only when specific fields change — not on every state emission
-
widgets used to stop rebuild propagation through the treeconst - Computed/derived state is calculated reactively, not stored redundantly
Subscriptions & disposal:
- All manual subscriptions (
) are cancelled in.listen()
/dispose()close() - Stream controllers are closed when no longer needed
- Timers are cancelled in disposal lifecycle
- Framework-managed lifecycle is preferred over manual subscription (declarative builders over
).listen() -
check beforemounted
in async callbackssetState -
not used afterBuildContext
without checkingawait
(Flutter 3.7+) — stale context causes crashescontext.mounted - No navigation, dialogs, or scaffold messages after async gaps without verifying the widget is still mounted
-
never stored in singletons, state managers, or static fieldsBuildContext
Local vs global state:
- Ephemeral UI state (checkbox, slider, animation) uses local state (
,setState
)ValueNotifier - Shared state is lifted only as high as needed — not over-globalized
- Feature-scoped state is properly disposed when the feature is no longer active
5. Performance
Unnecessary rebuilds:
-
not called at root widget level — localize state changessetState() -
widgets used to stop rebuild propagationconst -
used around complex subtrees that repaint independentlyRepaintBoundary -
child parameter used for subtrees independent of animationAnimatedBuilder
Expensive operations in build():
- No sorting, filtering, or mapping large collections in
— compute in state management layerbuild() - No regex compilation in
build() -
usage is specific (e.g.,MediaQuery.of(context)
)MediaQuery.sizeOf(context)
Image optimization:
- Network images use caching (any caching solution appropriate for the project)
- Appropriate image resolution for target device (no loading 4K images for thumbnails)
-
withImage.asset
/cacheWidth
to decode at display sizecacheHeight - Placeholder and error widgets provided for network images
Lazy loading:
-
/ListView.builder
used instead ofGridView.builder
for large or dynamic lists (concrete constructors are fine for small, static lists)ListView(children: [...]) - Pagination implemented for large data sets
- Deferred loading (
) used for heavy libraries in web buildsdeferred as
Other:
-
widget avoided in animations — useOpacity
orAnimatedOpacityFadeTransition - Clipping avoided in animations — pre-clip images
-
not overridden on widgets — useoperator ==
constructors insteadconst - Intrinsic dimension widgets (
,IntrinsicHeight
) used sparingly (extra layout pass)IntrinsicWidth
6. Testing
Test types and expectations:
- Unit tests: Cover all business logic (state managers, repositories, utility functions)
- Widget tests: Cover individual widget behavior, interactions, and visual output
- Integration tests: Cover critical user flows end-to-end
- Golden tests: Pixel-perfect comparisons for design-critical UI components
Coverage targets:
- Aim for 80%+ line coverage on business logic
- All state transitions have corresponding tests (loading → success, loading → error, retry, etc.)
- Edge cases tested: empty states, error states, loading states, boundary values
Test isolation:
- External dependencies (API clients, databases, services) are mocked or faked
- Each test file tests exactly one class/unit
- Tests verify behavior, not implementation details
- Stubs define only the behavior needed for each test (minimal stubbing)
- No shared mutable state between test cases
Widget test quality:
-
andpumpWidget
used correctly for async operationspump -
,find.byType
,find.text
used appropriatelyfind.byKey - No flaky tests depending on timing — use
or explicitpumpAndSettlepump(Duration) - Tests run in CI and failures block merges
7. Accessibility
Semantic widgets:
-
widget used to provide screen reader labels where automatic labels are insufficientSemantics -
used for purely decorative elementsExcludeSemantics -
used to combine related widgets into a single accessible elementMergeSemantics - Images have
property setsemanticLabel
Screen reader support:
- All interactive elements are focusable and have meaningful descriptions
- Focus order is logical (follows visual reading order)
Visual accessibility:
- Contrast ratio >= 4.5:1 for text against background
- Tappable targets are at least 48x48 pixels
- Color is not the sole indicator of state (use icons/text alongside)
- Text scales with system font size settings
Interaction accessibility:
- No no-op
callbacks — every button does something or is disabledonPressed - Error fields suggest corrections
- Context does not change unexpectedly while user is inputting data
8. Platform-Specific Concerns
iOS/Android differences:
- Platform-adaptive widgets used where appropriate
- Back navigation handled correctly (Android back button, iOS swipe-to-go-back)
- Status bar and safe area handled via
widgetSafeArea - Platform-specific permissions declared in
andAndroidManifest.xmlInfo.plist
Responsive design:
-
orLayoutBuilder
used for responsive layoutsMediaQuery - Breakpoints defined consistently (phone, tablet, desktop)
- Text doesn't overflow on small screens — use
,Flexible
,ExpandedFittedBox - Landscape orientation tested or explicitly locked
- Web-specific: mouse/keyboard interactions supported, hover states present
9. Security
Secure storage:
- Sensitive data (tokens, credentials) stored using platform-secure storage (Keychain on iOS, EncryptedSharedPreferences on Android)
- Never store secrets in plaintext storage
- Biometric authentication gating considered for sensitive operations
API key handling:
- API keys NOT hardcoded in Dart source — use
,--dart-define
files excluded from VCS, or compile-time configuration.env - Secrets not committed to git — check
.gitignore - Backend proxy used for truly secret keys (client should never hold server secrets)
Input validation:
- All user input validated before sending to API
- Form validation uses proper validation patterns
- No raw SQL or string interpolation of user input
- Deep link URLs validated and sanitized before navigation
Network security:
- HTTPS enforced for all API calls
- Certificate pinning considered for high-security apps
- Authentication tokens refreshed and expired properly
- No sensitive data logged or printed
10. Package/Dependency Review
Evaluating pub.dev packages:
- Check pub points score (aim for 130+/160)
- Check likes and popularity as community signals
- Verify the publisher is verified on pub.dev
- Check last publish date — stale packages (>1 year) are a risk
- Review open issues and response time from maintainers
- Check license compatibility with your project
- Verify platform support covers your targets
Version constraints:
- Use caret syntax (
) for dependencies — allows compatible updates^1.2.3 - Pin exact versions only when absolutely necessary
- Run
regularly to track stale dependenciesflutter pub outdated - No dependency overrides in production
— only for temporary fixes with a comment/issue linkpubspec.yaml - Minimize transitive dependency count — each dependency is an attack surface
Monorepo-specific (melos/workspace):
- Internal packages import only from public API — no
(breaks Dart package encapsulation)package:other/src/internal.dart - Internal package dependencies use workspace resolution, not hardcoded
relative stringspath: ../../ - All sub-packages share or inherit root
analysis_options.yaml
11. Navigation and Routing
General principles (apply to any routing solution):
- One routing approach used consistently — no mixing imperative
with a declarative routerNavigator.push - Route arguments are typed — no
orMap<String, dynamic>
castingObject? - Route paths defined as constants, enums, or generated — no magic strings scattered in code
- Auth guards/redirects centralized — not duplicated across individual screens
- Deep links configured for both Android and iOS
- Deep link URLs validated and sanitized before navigation
- Navigation state is testable — route changes can be verified in tests
- Back behavior is correct on all platforms
12. Error Handling
Framework error handling:
-
overridden to capture framework errors (build, layout, paint)FlutterError.onError -
set for async errors not caught by FlutterPlatformDispatcher.instance.onError -
customized for release mode (user-friendly instead of red screen)ErrorWidget.builder - Global error capture wrapper around
(e.g.,runApp
, Sentry/Crashlytics wrapper)runZonedGuarded
Error reporting:
- Error reporting service integrated (Firebase Crashlytics, Sentry, or equivalent)
- Non-fatal errors reported with stack traces
- State management error observer wired to error reporting (e.g., BlocObserver, ProviderObserver, or equivalent for your solution)
- User-identifiable info (user ID) attached to error reports for debugging
Graceful degradation:
- API errors result in user-friendly error UI, not crashes
- Retry mechanisms for transient network failures
- Offline state handled gracefully
- Error states in state management carry error info for display
- Raw exceptions (network, parsing) are mapped to user-friendly, localized messages before reaching the UI — never show raw exception strings to users
13. Internationalization (l10n)
Setup:
- Localization solution configured (Flutter's built-in ARB/l10n, easy_localization, or equivalent)
- Supported locales declared in app configuration
Content:
- All user-visible strings use the localization system — no hardcoded strings in widgets
- Template file includes descriptions/context for translators
- ICU message syntax used for plurals, genders, selects
- Placeholders defined with types
- No missing keys across locales
Code review:
- Localization accessor used consistently throughout the project
- Date, time, number, and currency formatting is locale-aware
- Text directionality (RTL) supported if targeting Arabic, Hebrew, etc.
- No string concatenation for localized text — use parameterized messages
14. Dependency Injection
Principles (apply to any DI approach):
- Classes depend on abstractions (interfaces), not concrete implementations at layer boundaries
- Dependencies provided externally via constructor, DI framework, or provider graph — not created internally
- Registration distinguishes lifetime: singleton vs factory vs lazy singleton
- Environment-specific bindings (dev/staging/prod) use configuration, not runtime
checksif - No circular dependencies in the DI graph
- Service locator calls (if used) are not scattered throughout business logic
15. Static Analysis
Configuration:
-
present with strict settings enabledanalysis_options.yaml - Strict analyzer settings:
,strict-casts: true
,strict-inference: truestrict-raw-types: true - A comprehensive lint rule set is included (very_good_analysis, flutter_lints, or custom strict rules)
- All sub-packages in monorepos inherit or share the root analysis options
Enforcement:
- No unresolved analyzer warnings in committed code
- Lint suppressions (
) are justified with comments explaining why// ignore: -
runs in CI and failures block mergesflutter analyze
Key rules to verify regardless of lint package:
-
— performance in widget treesprefer_const_constructors -
— use proper loggingavoid_print -
— prevent fire-and-forget async bugsunawaited_futures -
— immutability at variable levelprefer_final_locals -
— explicit contractsalways_declare_return_types -
— specific error handlingavoid_catches_without_on_clauses -
— consistent import stylealways_use_package_imports
State Management Quick Reference
The table below maps universal principles to their implementation in popular solutions. Use this to adapt review rules to whichever solution the project uses.
| Principle | BLoC/Cubit | Riverpod | Provider | GetX | MobX | Signals | Built-in |
|---|---|---|---|---|---|---|---|
| State container | / | / | | | | | |
| UI consumer | | | | / | | | |
| Selector | / | | | N/A | computed | | N/A |
| Side effects | | | callback | / | | | callbacks |
| Disposal | auto via | | auto via | | | manual | |
| Testing | | | directly | in test | store directly | signal directly | widget test |