Status: Accepted (2026-07-02). Closes OI-2 (App specs.md §12 / NFR-MAINT-2). Ratifies App TDD.md §2.3 Decision D3.
NFR-MAINT-2 requires the app to fix one declarative state-management approach and record it in an ADR before broad adoption; until then it is tracked as open item OI-2. The App design (TDD.md §2.3, D3) names Riverpod as the working choice with Bloc the documented alternative, and the whole app is structured around a single Connection seam (D1) that must be injectable as one overridable handle so every widget/golden test runs against a FakeConnection (D5).
App Sprint S1 (story A-05, the app shell + state foundation) cannot author its [red] tests or shell code until this choice is ratified: A-05's first acceptance criterion — "a single declarative state-management approach is chosen, recorded as an ADR, and applied consistently across the app" (NFR-MAINT-2) — makes the ADR a Definition-of-Ready blocker. This ADR discharges it.
The PyBLE app uses Riverpod (flutter_riverpod) as its single declarative state-management approach across every surface, from v1.0. Concretely:
- The active
Connectionis exposed through one root provider,connectionProvider(Provider<Connection>), overridden at theProviderScoperoot — with the realPbleConnectionin production (from A-02 / A-14) and with a scriptableFakeConnectionin tests and as the S1 shell stand-in. - All
Connection-derived runtime state (connection state, console buffer, run state, file listing, transfer progress) is exposed as derived providers read offconnectionProvider; widgetswatchderived providers and never hold a transport handle, aflutter_blue_plustype, or a wire type directly (D1/D2, CON-8, FR-BLE-8). - Durable user state stays in Drift (
lib/data/, D4); Riverpod carries runtime/UI state only. Ephemeral view state (active tab, selection, scroll) stays in widget/controller scope. - This is applied consistently: no surface may introduce a second state-management library (Bloc, GetX,
provider, hand-rolledInheritedWidgetgraphs) for shared/app state.
Unchanged by this ADR: the Connection seam (D1), the neutral types (D2), the FakeConnection-driven test model (D5), the strict UI → lib/pble → lib/ble layering (NFR-MAINT-1), and the clean-room / no-leak rule.
- Bloc / Cubit — the documented alternative (TDD D3). Mature, explicit event→state modelling, strong testability. Not chosen as the default because injecting a single
Connectionvia a compile-safe provider override and deriving read-only providers off one seam is more direct in Riverpod, with the least ceremony for deterministic widget/golden tests. Bloc remains the viable fallback; adopting it would be a new ADR superseding this one. provider(InheritedWidget wrapper) — lighter, but lacks compile-safe overrides and auto-dispose and scales poorly across many derived states.- GetX / other batteries-included frameworks — rejected: opaque global state, weaker testability, and (GetX) navigation/DI coupling that a clean-room, seam-tested app does not want.
- Raw
setState/InheritedWidget— insufficient for shared cross-surface state and the single-seam injection pattern.
Positive
- One overridable
connectionProvideris the injection point forFakeConnection, so every widget/golden/unit test drives UI with no BLE and no hardware (FR-CONN-7, D5) — the structural precondition for Test-Driven Development. - State flows down from
Connection, intents flow up through it, keeping the strict layering (NFR-MAINT-1, CON-8) observable and the editor/console/files transport-agnostic. - A single approach keeps the codebase uniform (NFR-MAINT-2) and golden tests deterministic (one provider to override).
Costs / mitigations
- The team standardizes on Riverpod idioms. Mitigation: the S1 shell (A-05) lands the canonical provider set +
ProviderScopewiring the other engineers copy; the import-boundary + no-leak gates keep the seam clean. - S1 pins Riverpod 2.x; a later move to a new major (3.x) is a dependency-ledger change with rationale, not a re-decision of the approach.
Process
- OI-2 is closed; NFR-MAINT-2 is satisfied by this ADR.
flutter_riverpodis the only non-SDK app dependency permitted in Sprint S1 (dependency governance, BLD-2); it is MIT-licensed and recorded inapp/DEPENDENCIES.md.- Spec-first: this ADR lands in its own
[docs]commit before A-05's[red]/[green], per SDD.