Mobile Flutter Core
Shared model for the mobile-flutter cluster. The pattern, review, and architecture spokes all
depend on these decisions — keep them consistent here so no spoke contradicts another.
1. The decision this cluster turns on: state management
Everything else (immutability, testing, rebuild discipline, DI) follows from which state
solution you pick. Pick one per app and stay consistent. The two families:
- Immutable-state (BLoC/Cubit, Riverpod, Redux) — state is replaced, never mutated; new
instances via
copyWith/constructors; ==/hashCode over all fields. UI rebuilds when a new
value is emitted.
- Reactive-mutation (MobX, GetX, Signals) — state is mutated through a tracked API
(
@action, .value, .obs); derived values use the computed mechanism; reactions/disposers
must be cleaned up.
| Concern |
BLoC/Cubit |
Riverpod |
Provider |
GetX |
MobX |
Signals |
| Container |
Bloc/Cubit |
Notifier/AsyncNotifier |
ChangeNotifier |
GetxController |
Store |
signal() |
| UI consumer |
BlocBuilder |
ConsumerWidget |
Consumer |
Obx/GetBuilder |
Observer |
Watch |
| Selector |
buildWhen/BlocSelector |
ref.watch(p.select) |
Selector |
— |
computed |
computed() |
| Disposal |
auto via BlocProvider |
.autoDispose |
auto via Provider |
onClose() |
ReactionDisposer |
manual |
| Testing |
blocTest() |
ProviderContainer |
directly |
Get.put |
store directly |
signal directly |
Default recommendation: Riverpod or BLoC for non-trivial apps (immutable, testable,
explicit async states). Implementation patterns → dart-flutter-patterns; review rules per
solution → flutter-dart-code-review (§4).
2. Immutable-state contract (the rule the decision implies)
- Model mutually-exclusive states as sealed types / union variants (or the solution's async
type like Riverpod
AsyncValue) — never boolean flags (isLoading + hasError is an
impossible-state generator).
- Every async op models loading / success / error as distinct states; error states carry the
error, loading states don't carry stale data.
- Collections inside state are exposed as unmodifiable views, not raw
List/Map.
- Handle every variant exhaustively in the UI (Dart 3
switch expression enforces this).
3. Layered architecture (shared across Flutter & native/KMP)
Business logic lives outside the widget/UI layer, behind injected dependencies, with a
repository abstracting data sources. Dependency direction points inward:
presentation ──> domain <── data
│ ▲ │
└── design ──┘ └── DB / network
domain depends on nothing framework-specific (pure Dart / pure Kotlin).
- Flutter UI structure (widget decomposition, clean layering) →
dart-flutter-patterns.
- Native Android / Kotlin-Multiplatform modules, UseCases, Repositories, mappers, DI →
android-clean-architecture.
- Rule: never expose DB entities/DTOs to the UI — map to domain models; keep
domain import-pure.
4. Async & lifecycle safety (cross-cutting, non-negotiable)
- After any
await in a StatefulWidget, check mounted (or context.mounted, Flutter
3.7+) before touching BuildContext — navigation, dialogs, ScaffoldMessenger.
- Cancel every manual subscription (
.listen()), close stream controllers, cancel timers in
dispose()/close(). Prefer declarative builders over manual subscriptions.
- Never store
BuildContext in singletons, controllers, or static fields.
- Run futures concurrently (
Future.wait / record .wait) instead of awaiting sequentially.
5. Version / tooling matrix
| Layer |
Target |
Notes |
| Language |
Dart 3+ |
sealed classes, records, patterns, exhaustive switch are assumed |
| Flutter |
3.7+ |
context.mounted guard; current widget/perf APIs |
| Codegen |
freezed + *.g.dart |
generated files current or git-ignored |
| Routing |
GoRouter (declarative) |
one routing approach per app — no mixing imperative + declarative |
| Networking |
Dio (interceptors, one-time refresh-retry guard) |
secure storage for tokens |
| Native / KMP |
Kotlin + Room (Android) / SQLDelight + Ktor (KMP) |
DI via Koin (KMP) or Hilt (Android) |
| Lint |
strict analysis_options.yaml |
strict-casts/inference/raw-types; very_good_analysis or flutter_lints |
6. Shared guardrails
- One state-management solution and one routing approach per app; stay consistent.
- Business logic out of widgets; dependencies injected, never constructed in-place.
- Sealed/union states over boolean flags; exhaustive handling in UI.
mounted-check BuildContext after every await; dispose every subscription.
- Keep
domain framework-pure; map entities/DTOs → domain models at the boundary.
- Secrets via
--dart-define/secure storage — never hardcoded in Dart, never logged.
- Run
flutter analyze + the test suite in CI; failures block merges → flutter-dart-code-review.
- Flutter vs native/KMP: pick Flutter for a single cross-platform UI codebase; reach for
android-clean-architecture (KMP) when you need shared business logic with fully native UI.
1---2name: mobile-flutter-core3description: Shared reference for the mobile-flutter cluster: the state-management decision (which solution, and the immutable-state contract it implies), the layered-architecture rule, the async/lifecycle safety rules, and the Dart/Flutter/KMP version & tooling matrix. USE WHEN choosing a state solution, designing the data/domain boundary, or aligning Flutter/Android/KMP tooling — the decisions every spoke shares.4---56# Mobile Flutter Core78Shared model for the `mobile-flutter` cluster. The pattern, review, and architecture spokes all9depend on these decisions — keep them consistent here so no spoke contradicts another.1011## 1. The decision this cluster turns on: state management1213Everything else (immutability, testing, rebuild discipline, DI) follows from **which state14solution you pick**. Pick **one per app** and stay consistent. The two families:1516- **Immutable-state** (BLoC/Cubit, Riverpod, Redux) — state is replaced, never mutated; new17 instances via `copyWith`/constructors; `==`/`hashCode` over all fields. UI rebuilds when a new18 value is emitted.19- **Reactive-mutation** (MobX, GetX, Signals) — state is mutated through a tracked API20 (`@action`, `.value`, `.obs`); derived values use the computed mechanism; reactions/disposers21 must be cleaned up.2223| Concern | BLoC/Cubit | Riverpod | Provider | GetX | MobX | Signals |24|---|---|---|---|---|---|---|25| Container | `Bloc`/`Cubit` | `Notifier`/`AsyncNotifier` | `ChangeNotifier` | `GetxController` | `Store` | `signal()` |26| UI consumer | `BlocBuilder` | `ConsumerWidget` | `Consumer` | `Obx`/`GetBuilder` | `Observer` | `Watch` |27| Selector | `buildWhen`/`BlocSelector` | `ref.watch(p.select)` | `Selector` | — | `computed` | `computed()` |28| Disposal | auto via `BlocProvider` | `.autoDispose` | auto via `Provider` | `onClose()` | `ReactionDisposer` | manual |29| Testing | `blocTest()` | `ProviderContainer` | directly | `Get.put` | store directly | signal directly |3031**Default recommendation:** Riverpod or BLoC for non-trivial apps (immutable, testable,32explicit async states). Implementation patterns → `dart-flutter-patterns`; review rules per33solution → `flutter-dart-code-review` (§4).3435## 2. Immutable-state contract (the rule the decision implies)3637- Model mutually-exclusive states as **sealed types / union variants** (or the solution's async38 type like Riverpod `AsyncValue`) — never boolean flags (`isLoading` + `hasError` is an39 impossible-state generator).40- Every async op models **loading / success / error** as distinct states; error states carry the41 error, loading states don't carry stale data.42- Collections inside state are exposed as **unmodifiable views**, not raw `List`/`Map`.43- Handle every variant exhaustively in the UI (Dart 3 `switch` expression enforces this).4445## 3. Layered architecture (shared across Flutter & native/KMP)4647Business logic lives **outside the widget/UI layer**, behind injected dependencies, with a48repository abstracting data sources. Dependency direction points inward:4950```51presentation ──> domain <── data52 │ ▲ │53 └── design ──┘ └── DB / network54domain depends on nothing framework-specific (pure Dart / pure Kotlin).55```5657- Flutter UI structure (widget decomposition, clean layering) → `dart-flutter-patterns`.58- Native Android / Kotlin-Multiplatform modules, UseCases, Repositories, mappers, DI → `android-clean-architecture`.59- **Rule:** never expose DB entities/DTOs to the UI — map to domain models; keep `domain` import-pure.6061## 4. Async & lifecycle safety (cross-cutting, non-negotiable)6263- After **any `await`** in a `StatefulWidget`, check `mounted` (or `context.mounted`, Flutter64 3.7+) before touching `BuildContext` — navigation, dialogs, `ScaffoldMessenger`.65- Cancel every manual subscription (`.listen()`), close stream controllers, cancel timers in66 `dispose()`/`close()`. Prefer declarative builders over manual subscriptions.67- Never store `BuildContext` in singletons, controllers, or static fields.68- Run futures concurrently (`Future.wait` / record `.wait`) instead of awaiting sequentially.6970## 5. Version / tooling matrix7172| Layer | Target | Notes |73|---|---|---|74| Language | **Dart 3+** | sealed classes, records, patterns, exhaustive `switch` are assumed |75| Flutter | 3.7+ | `context.mounted` guard; current widget/perf APIs |76| Codegen | `freezed` + `*.g.dart` | generated files current or git-ignored |77| Routing | GoRouter (declarative) | one routing approach per app — no mixing imperative + declarative |78| Networking | Dio (interceptors, one-time refresh-retry guard) | secure storage for tokens |79| Native / KMP | Kotlin + Room *(Android)* / SQLDelight + Ktor *(KMP)* | DI via Koin (KMP) or Hilt (Android) |80| Lint | strict `analysis_options.yaml` | `strict-casts/inference/raw-types`; `very_good_analysis` or `flutter_lints` |8182## 6. Shared guardrails8384- **One** state-management solution and **one** routing approach per app; stay consistent.85- Business logic out of widgets; dependencies injected, never constructed in-place.86- Sealed/union states over boolean flags; exhaustive handling in UI.87- `mounted`-check `BuildContext` after every `await`; dispose every subscription.88- Keep `domain` framework-pure; map entities/DTOs → domain models at the boundary.89- Secrets via `--dart-define`/secure storage — never hardcoded in Dart, never logged.90- Run `flutter analyze` + the test suite in CI; failures block merges → `flutter-dart-code-review`.91- Flutter vs native/KMP: pick Flutter for a single cross-platform UI codebase; reach for92 `android-clean-architecture` (KMP) when you need shared business logic with fully native UI.