Skip to main content

State Management with Riverpod

The app uses Riverpod with an explicit action layer: Screen/Widget -> ViewModel/Provider -> Application Action -> Repository -> Drift For the full app directory map and layer walkthrough, see App architecture.

Architecture

The Flutter app follows a Feature-Sliced Architecture (FSA) where shared concerns live in core/ and product features are isolated in self-contained folders in features/. Within each feature slice, layers depend only on the domain layer.
  • UI / Presentation: Flutter widgets/screens and feature ViewModels. Exposes UI state and dispatches user actions.
  • Application: Command and query actions (one class per operation).
  • Domain: Pure models and abstract repository contracts.
  • Data / Persistence: Drift repository implementations.
ViewModels do not call Drift databases directly.

Application Actions

Application actions act as the transactional boundary between presentation ViewModels and repositories. Rules:
  • One class per operation.
  • Verb-phrase class names (e.g. CreateTask).
  • No UseCase suffix.
  • call(...) API with named parameters.
  • All actions include privacy-safe diagnostics logging.
  • Futures return ApplicationResult<T> (Success<T> / Failure<T>).
Logging conventions:
  • Mutation actions log info on start/success.
  • Read and compute actions log debug on start/success.
  • Watch actions log debug when a subscription starts and when it terminates.
  • Validation / not-found / conflict failures log warning.
  • Unexpected / infrastructure failures log error.
  • Logs must include metadata only: IDs, counts, date windows, and filter presence.
  • Logs must never include task titles, raw event payloads, or raw check-in text values (body / mind / mood contents).
Examples:
  • CreateTask
  • SaveCheckIn
  • GetEventsInWindow

Repository Contracts

Repository write contracts use named parameters directly.
  • No write DTO classes in repository interfaces.
  • Field-level partial updates use FieldUpdate<T>.
  • Repository adapters in core/persistence/ handle schema mappings, transactions, and event triggers.

Provider Wiring

Core / Bootstrap Providers

Core infrastructure and action bindings live under lib/core/bootstrap/providers/:
  • repository_providers.dart: Instantiates the concrete Drift repositories.
  • logger_provider.dart: Global diagnostic logs.
  • Feature action providers (e.g. task_actions_providers.dart, mana_actions_providers.dart): One provider per application action class.

Feature Providers & ViewModels

New query state or ViewModel providers live inside feature slices (e.g. lib/features/home/presentation/providers/) and use Riverpod code generation:
part 'home_providers.g.dart';

@riverpod
Future<HomeViewData> homeView(Ref ref) async {
  // Watch or read actions to fetch domain data
}
Generated files (*.g.dart) are kept beside the source code.

ViewModel Conventions

ViewModels are implemented using class-based @riverpod notifiers (e.g., inheriting from _$MyViewModel):
  • Commands are methods on the ViewModel class.
  • Commands return Future<ApplicationResult<T>>.
  • ViewModels read action providers, never database/repository providers directly.

Folder Layout

lib/
  ├── core/                             # Shared infrastructure
  │   ├── bootstrap/
  │   │   └── providers/                # Action bindings & singletons
  │   ├── design/                       # Tokens & primitives
  │   ├── diagnostics/                  # Logging wrappers
  │   ├── domain/                       # Core models & repositories
  │   ├── persistence/                  # Drift tables & DB repositories
  │   └── platform/                     # Platform wrappers

  └── features/                         # Feature slices
      └── <feature>/
          ├── domain/                   # Local models & policies
          ├── application/              # Local actions
          └── presentation/             # Presentation layer
              ├── view_models/          # Riverpod state notifier ViewModels
              ├── providers/            # Generated UI providers
              └── widgets/              # UI widgets and screens

Naming

ThingConventionExample
Application actionVerb phrase, no suffixCreateTask, GetTaskById
Repository interface{Entity}RepositoryTaskRepository
Drift implementationDrift prefixDriftTaskRepository
ViewModel{Concern}ViewModelAddTaskViewModel
ViewModel provider{concern}ViewModelProvideraddTaskViewModelProvider (generated)

Async Patterns

  • Reactive reads: StreamProvider + watch actions (e.g., taskListProvider).
  • Point-in-time reads/writes: Future + ApplicationResult<T>.
  • No raw Drift query exposure above data adapters.
  • Every async state consumed in widgets must explicitly handle loading, error, and data states.