Flutter Architecture Skill: Modular + Riverpod 3.x (DB-First + UX Pessimista)
Goal
Ensure all Flutter changes follow the project architecture:
- DB-First: UI never consumes MQTT directly; UI reacts only to DB (Drift) changes.
- UX Pessimista: commands show loading immediately, confirm only after DB updates, and must timeout safely.
- Clear separation: Modular = Infrastructure + Routes, Riverpod = UI State + ViewModels + Lifecycle.
- Riverpod 3.x migration compliant (Option B) while documenting Legacy patterns.
When to use this skill
Use this skill when:
- Adding/changing Notifiers / Providers (Riverpod 3.x).
- Implementing features under
lib/modules/*.
- Implementing MQTT sync (MQTT → DB) or command flow (UI → Repo → MQTT → DB → UI).
- Refactoring legacy
ChangeNotifier / StateNotifier / old providers.
- Reviewing PRs touching: Modular modules, DB schema/DAOs, MQTT topics, session context, resource parsing.
Non-goals
This skill does not:
- Modify MQTT contract, ACLs, provisioning rules, firmware behavior.
- Decide business/security logic that belongs to firmware or orchestrator.
- Define UI/UX layout details beyond DB-first + pessimistic UX rules.
Core Principles (Mandatory)
1) DB-First (Source of Truth)
Required flow: MQTT → Drift DB → Riverpod Notifier → Widgets
Rules:
- Widgets must not parse MQTT payloads or subscribe to MQTT streams.
- The MQTT layer must write data/state/meta into DB via a sync service.
- ViewModels/Notifiers read DB watch streams and expose derived UI state.
2) UX Pessimista (Commands)
Rules:
- On user action: set loading immediately.
- Only confirm success when DB state reflects the change.
- Apply timeout (10s default) to avoid infinite loading.
- Failures must surface via state or a global error notifier (snackbar/toast).
3) Separation of Responsibilities
- Flutter Modular
- Routes
- Dependency Injection of infra singletons/services
- Repositories (concrete implementations)
- Riverpod
- UI state
- ViewModels (Notifiers)
- Lifecycle (autoDispose)
- Widgets
- “Dumb”: render + delegate actions to ViewModels
Modular vs Riverpod Rules (Definitive)
Rule A — Infra lives in Modular
Bind in Modular:
- AppDatabase (Drift)
- MQTT client/service
- MqttRemoteDataSource / Sync service (MQTT → DB)
- Command manager
- Repositories (concrete) without Ref/WidgetRef dependency
- Logging, serializers, adapters
Rule B — UI State lives in Riverpod
- Prefer Riverpod 3.x Notifiers for ViewModels.
- Use
.autoDispose.family when state depends on a resourceId, deviceId, or similar.
Rule C — Repository must NOT depend on Ref/WidgetRef
- Repositories must be constructible by Modular without Riverpod.
- If repository needs tenant/home context:
- Use a SessionContextProvider (adapter owned by Riverpod, consumed by repo), OR
- Require context as explicit method args (
sendCommand(tenantId, homeId, ...))
Rule D — One “bridge” only
If context is needed across layers, use a single adapter:
SessionContextProvider (current tenant/home) exposed by Riverpod, injected into repositories via Modular.
Riverpod 3.x Standard (Option B)
Provider Types (Standard)
- Stateful ViewModels:
Notifier + NotifierProvider.autoDispose.family
- Pure derived values:
Provider
- Streams:
StreamProvider only when truly streaming (DB watch is usually handled inside Notifier)
Canonical pattern (family Notifier)
File: lib/modules/<feature>/presentation/providers/<x>_providers.dart
final pumpNotifierProvider =
NotifierProvider.autoDispose
.family<PumpNotifier, PumpState, String>(PumpNotifier.new);
File: `lib/modules/<feature>/presentation/viewmodels/pump_notifier.dart`
- `build(resourceId)` wires DB watchers and sets initial state.
## Notifier Responsibilities
In `build(arg)`:
- Read DB + current home from providers
- Start DB watcher (Drift `.watch*`)
- Register `ref.onDispose` cleanup
- Return initial state (loading/stale by default)
Actions:
- Call repository methods
- Manage command timeout
- Never update UI based on publish success alone—wait for DB change.
* * *
# Legacy Patterns (Document but avoid new usage)
## Legacy A — ChangeNotifier / ChangeNotifierProvider
Allowed only for existing legacy code, during migration.
Rules:
- No new features should start in ChangeNotifier.
- If a legacy ChangeNotifier must remain, ensure:
- It still follows DB-first and pessimistic UX
- It cancels subscriptions/timers on dispose
- There is a migration note for future conversion to Notifier
## Legacy B — Provider.autoDispose used as workaround (Not reactive)
Using plain `Provider.autoDispose` for ViewModels is forbidden for live data screens
because UI will not rebuild when state changes.
* * *
# Decision Tree (Pick the right approach)
1. Is this UI showing live sensor/actuator data?
- Yes → Use `NotifierProvider.autoDispose.family` + DB watcher inside Notifier.
- No → Use `Provider` or simple computed provider.
1. Does the feature send commands?
- Yes → Add pessimistic UX logic + timeout + wait for DB update.
1. Does repository need tenant/home?
- Yes → Use SessionContextProvider adapter OR pass args explicitly.
- No → Keep repository pure.
1. Is it new code?
- Yes → Riverpod 3.x Notifier pattern only.
- No (legacy) → keep stable but add migration note.
* * *
# Code Review Checklist (PR Acceptance Criteria)
## Architecture
- <input disabled="" type="checkbox"> Modular contains infra binds (db/mqtt/services/repo) and routes.
- <input disabled="" type="checkbox"> Riverpod contains UI state + Notifiers.
- <input disabled="" type="checkbox"> No repository depends on `Ref`/`WidgetRef`.
## DB-First
- <input disabled="" type="checkbox"> No widget reads MQTT directly.
- <input disabled="" type="checkbox"> UI reacts only via DB watchers (Drift) exposed by Notifier state.
## UX Pessimista
- <input disabled="" type="checkbox"> Commands set loading immediately.
- <input disabled="" type="checkbox"> Success is confirmed only when DB updates.
- <input disabled="" type="checkbox"> Timeout prevents infinite loading.
- <input disabled="" type="checkbox"> Failures propagate to UI (state or global error notifier).
## Lifecycle
- <input disabled="" type="checkbox"> All stream subscriptions and timers are cancelled via `ref.onDispose`.
## Consistency
- <input disabled="" type="checkbox"> No duplicated subscription refresh calls.
- <input disabled="" type="checkbox"> No duplicate device/resource inserts (verify DAO conflict targets).
- <input disabled="" type="checkbox"> Resource IDs treated as immutable (contract).
* * *
# Anti-patterns (Block the PR)
- Widget parsing MQTT payload or subscribing MQTT directly.
- Widget calling repository to send command (must go through ViewModel/Notifier).
- Repository depending on `ref` or reading session providers directly.
- “Optimistic UI”: toggling state in UI without DB confirmation.
- Provider used instead of Notifier for live-changing ViewModel state.
- Notifier/VM missing dispose cleanup.
* * *
# Integration Notes (Project-specific)
## DB-first + MQTT sync
- `MqttRemoteDataSource`: raw MQTT IO + parsing → emits structured streams.
- `MqttSyncService`: consumes those streams and persists into Drift (write-only).
- Notifiers: read Drift watchers and compute UI state.
## Session context
- Tenant/Home selection must trigger:
- Subscription refresh (topics for that tenant/home)
- Sync service start with current home id
* * *
# Output format (When asked to implement)
When implementing/refactoring under this skill:
- Specify **file name**
- Specify **where to place it**
- Specify **purpose**
- Provide complete code that compiles
- Call out any contract constraints explicitly (resourceId immutável, meta topics, etc.)
---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/marcoeli) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-14 -->
1---2name: marcoeli-cii-flutter-arch-modular-riverpod3description: Flutter Architecture Skill: Modular + Riverpod 3.x (DB-First + UX Pessimista)4---56# Flutter Architecture Skill: Modular + Riverpod 3.x (DB-First + UX Pessimista)78## Goal9Ensure all Flutter changes follow the project architecture:10- **DB-First**: UI never consumes MQTT directly; UI reacts only to DB (Drift) changes.11- **UX Pessimista**: commands show loading immediately, confirm only after DB updates, and must timeout safely.12- **Clear separation**: **Modular = Infrastructure + Routes**, **Riverpod = UI State + ViewModels + Lifecycle**.13- **Riverpod 3.x migration compliant** (Option B) while documenting **Legacy** patterns.1415## When to use this skill16Use this skill when:17- Adding/changing **Notifiers / Providers** (Riverpod 3.x).18- Implementing **features** under `lib/modules/*`.19- Implementing **MQTT sync** (MQTT → DB) or **command flow** (UI → Repo → MQTT → DB → UI).20- Refactoring legacy `ChangeNotifier` / `StateNotifier` / old providers.21- Reviewing PRs touching: Modular modules, DB schema/DAOs, MQTT topics, session context, resource parsing.2223## Non-goals24This skill does **not**:25- Modify MQTT contract, ACLs, provisioning rules, firmware behavior.26- Decide business/security logic that belongs to firmware or orchestrator.27- Define UI/UX layout details beyond DB-first + pessimistic UX rules.2829---3031# Core Principles (Mandatory)3233## 1) DB-First (Source of Truth)34**Required flow**: `MQTT → Drift DB → Riverpod Notifier → Widgets`3536Rules:37- Widgets must **not** parse MQTT payloads or subscribe to MQTT streams.38- The MQTT layer must write data/state/meta into DB via a sync service.39- ViewModels/Notifiers read **DB watch streams** and expose derived UI state.4041## 2) UX Pessimista (Commands)42Rules:43- On user action: set loading **immediately**.44- Only confirm success when DB state reflects the change.45- Apply **timeout (10s default)** to avoid infinite loading.46- Failures must surface via state or a global error notifier (snackbar/toast).4748## 3) Separation of Responsibilities49- **Flutter Modular**50 - Routes51 - Dependency Injection of infra singletons/services52 - Repositories (concrete implementations)53- **Riverpod**54 - UI state55 - ViewModels (Notifiers)56 - Lifecycle (autoDispose)57- **Widgets**58 - “Dumb”: render + delegate actions to ViewModels5960---6162# Modular vs Riverpod Rules (Definitive)6364## Rule A — Infra lives in Modular65Bind in Modular:66- AppDatabase (Drift)67- MQTT client/service68- MqttRemoteDataSource / Sync service (MQTT → DB)69- Command manager70- Repositories (concrete) **without Ref/WidgetRef dependency**71- Logging, serializers, adapters7273## Rule B — UI State lives in Riverpod74- Prefer **Riverpod 3.x Notifiers** for ViewModels.75- Use `.autoDispose.family` when state depends on a `resourceId`, `deviceId`, or similar.7677## Rule C — Repository must NOT depend on `Ref`/`WidgetRef`78- Repositories must be constructible by Modular without Riverpod.79- If repository needs tenant/home context:80 - Use a **SessionContextProvider** (adapter owned by Riverpod, consumed by repo), OR81 - Require context as explicit method args (`sendCommand(tenantId, homeId, ...)`)8283## Rule D — One “bridge” only84If context is needed across layers, use a single adapter:85- `SessionContextProvider` (current tenant/home) exposed by Riverpod, injected into repositories via Modular.8687---8889# Riverpod 3.x Standard (Option B)9091## Provider Types (Standard)92- **Stateful ViewModels**: `Notifier` + `NotifierProvider.autoDispose.family`93- **Pure derived values**: `Provider`94- **Streams**: `StreamProvider` only when truly streaming (DB watch is usually handled inside Notifier)9596### Canonical pattern (family Notifier)97File: `lib/modules/<feature>/presentation/providers/<x>_providers.dart`9899```dart100final pumpNotifierProvider =101 NotifierProvider.autoDispose102 .family<PumpNotifier, PumpState, String>(PumpNotifier.new);103File: `lib/modules/<feature>/presentation/viewmodels/pump_notifier.dart`104105- `build(resourceId)` wires DB watchers and sets initial state.106107## Notifier Responsibilities108109In `build(arg)`:110111- Read DB + current home from providers112- Start DB watcher (Drift `.watch*`)113- Register `ref.onDispose` cleanup114- Return initial state (loading/stale by default)115116Actions:117118- Call repository methods119- Manage command timeout120- Never update UI based on publish success alone—wait for DB change.121122* * *123124# Legacy Patterns (Document but avoid new usage)125126## Legacy A — ChangeNotifier / ChangeNotifierProvider127128Allowed only for existing legacy code, during migration.129130Rules:131132- No new features should start in ChangeNotifier.133- If a legacy ChangeNotifier must remain, ensure:134135 - It still follows DB-first and pessimistic UX136 - It cancels subscriptions/timers on dispose137 - There is a migration note for future conversion to Notifier138139## Legacy B — Provider.autoDispose used as workaround (Not reactive)140141Using plain `Provider.autoDispose` for ViewModels is forbidden for live data screens 142because UI will not rebuild when state changes.143144* * *145146# Decision Tree (Pick the right approach)1471481. Is this UI showing live sensor/actuator data?149150- Yes → Use `NotifierProvider.autoDispose.family` + DB watcher inside Notifier.151- No → Use `Provider` or simple computed provider.1521531. Does the feature send commands?154155- Yes → Add pessimistic UX logic + timeout + wait for DB update.1561571. Does repository need tenant/home?158159- Yes → Use SessionContextProvider adapter OR pass args explicitly.160- No → Keep repository pure.1611621. Is it new code?163164- Yes → Riverpod 3.x Notifier pattern only.165- No (legacy) → keep stable but add migration note.166167* * *168169# Code Review Checklist (PR Acceptance Criteria)170171## Architecture172173- <input disabled="" type="checkbox"> Modular contains infra binds (db/mqtt/services/repo) and routes.174- <input disabled="" type="checkbox"> Riverpod contains UI state + Notifiers.175- <input disabled="" type="checkbox"> No repository depends on `Ref`/`WidgetRef`.176177## DB-First178179- <input disabled="" type="checkbox"> No widget reads MQTT directly.180- <input disabled="" type="checkbox"> UI reacts only via DB watchers (Drift) exposed by Notifier state.181182## UX Pessimista183184- <input disabled="" type="checkbox"> Commands set loading immediately.185- <input disabled="" type="checkbox"> Success is confirmed only when DB updates.186- <input disabled="" type="checkbox"> Timeout prevents infinite loading.187- <input disabled="" type="checkbox"> Failures propagate to UI (state or global error notifier).188189## Lifecycle190191- <input disabled="" type="checkbox"> All stream subscriptions and timers are cancelled via `ref.onDispose`.192193## Consistency194195- <input disabled="" type="checkbox"> No duplicated subscription refresh calls.196- <input disabled="" type="checkbox"> No duplicate device/resource inserts (verify DAO conflict targets).197- <input disabled="" type="checkbox"> Resource IDs treated as immutable (contract).198199* * *200201# Anti-patterns (Block the PR)202203- Widget parsing MQTT payload or subscribing MQTT directly.204- Widget calling repository to send command (must go through ViewModel/Notifier).205- Repository depending on `ref` or reading session providers directly.206- “Optimistic UI”: toggling state in UI without DB confirmation.207- Provider used instead of Notifier for live-changing ViewModel state.208- Notifier/VM missing dispose cleanup.209210* * *211212# Integration Notes (Project-specific)213214## DB-first + MQTT sync215216- `MqttRemoteDataSource`: raw MQTT IO + parsing → emits structured streams.217- `MqttSyncService`: consumes those streams and persists into Drift (write-only).218- Notifiers: read Drift watchers and compute UI state.219220## Session context221222- Tenant/Home selection must trigger:223224 - Subscription refresh (topics for that tenant/home)225 - Sync service start with current home id226227* * *228229# Output format (When asked to implement)230231When implementing/refactoring under this skill:232233- Specify **file name**234- Specify **where to place it**235- Specify **purpose**236- Provide complete code that compiles237- Call out any contract constraints explicitly (resourceId immutável, meta topics, etc.)238239---240> Converted and distributed by [TomeVault](https://tomevault.io/claim/marcoeli) — claim your Tome and manage your conversions.241<!-- tomevault:4.0:skill_md:2026-04-14 -->