ADR-0042: Riverpod for dependency injection and state management

Status

Accepted (pending FieldForce Phase 1 implementation)

Tags

fieldforce, digital-worker, mobile, flutter, state-management, dependency-injection, riverpod, codegen

Decision

The Flutter apps use Riverpod (code-generation @riverpod syntax) as the single dependency-injection and state-management solution, committed across the whole workspace. No mixing with Bloc, Provider, or raw setState for shared/app state.
  • Riverpod is the composition root — concrete adapters (PowerSync repositories, API clients, config) are wired in and overridable for tests.
  • Reactive synced data is exposed via StreamProvider / stream-based notifiers so PowerSync’s realtime reads flow to the UI automatically (ADR-0040).
  • All providers are generated via @riverpod — no hand-written Provider(...) declarations.

Why

The architecture (ADR-0039) needs a composition root that performs dependency inversion (wiring data-layer adapters behind domain ports) and a reactive state layer that re-renders on PowerSync stream changes. Riverpod provides both: compile-safe DI with trivial test overrides, and reactive providers that integrate naturally with stream-based reactive reads. For a vibe-coded, solo/lead project the priority is the lightest tool the LLM generates cleanly and consistently. The biggest failure mode in Flutter codebases is mixing several state tools (Riverpod + Provider + Bloc + setState) because each generated feature improvised differently. Committing to one — and pinning it — is what keeps generated code coherent. Riverpod’s code-gen @riverpod produces consistent, type-safe providers with less boilerplate than Bloc’s event/state ceremony. Rejected alternatives:
  • Bloc / flutter_bloc. Strong event/state discipline favored by larger teams, but more boilerplate and ceremony than a solo vibe-coded project needs; heavier generated code. Rejected for this project’s profile.
  • Provider (vanilla). Lighter but weaker compile-time guarantees and test ergonomics than Riverpod; largely superseded. Rejected.
  • Raw setState / InheritedWidget for shared state. Doesn’t scale to app-wide DI or reactive sync. Rejected for shared state (local-only widget state may still use setState).
  • Mixing multiple solutions. The primary failure mode to avoid. Explicitly rejected.

Consequences

  • Relies on codegen (build_runner, riverpod_generator) — consistent with the codegen discipline (CLAUDE.md §15); generated files are committed.
  • The team commits to Riverpod idioms throughout; introducing a second state tool is a design break.
  • Reactive sync integration depends on stream-based providers; repositories should expose Stream<T> for synced reads.

Rules for agents

  • Use Riverpod (@riverpod code-gen) for ALL dependency injection and shared/app state.
  • Do NOT introduce Bloc, vanilla Provider, or use setState for shared state. Local-only ephemeral widget state may use setState.
  • No hand-written Provider(...) — generate providers via @riverpod.
  • Wire concrete adapters at the composition root; expose synced data via stream providers.
  • Override providers for tests rather than constructing real adapters.