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, codegenDecision
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-writtenProvider(...)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 usesetState). - 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 (
@riverpodcode-gen) for ALL dependency injection and shared/app state. - Do NOT introduce Bloc, vanilla Provider, or use
setStatefor shared state. Local-only ephemeral widget state may usesetState. - 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.