Skip to main content

State Management with Riverpod

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

Architecture

The Flutter app follows layered boundaries where each layer depends only on the next layer down.
  • UI: widgets/screens + feature ViewModels
  • Domain application: one class per operation (actions)
  • Domain repositories: abstract contracts
  • Data repositories: Drift-backed implementations
ViewModels do not call Drift repositories directly.

Application Actions

Application actions are the boundary between ViewModels and repositories. Rules:
  • One class per operation
  • Verb-phrase class names
  • No UseCase suffix
  • call(...) API with named parameters
  • All actions include 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 not include task titles, raw event payloads, or raw check-in body / mind / mood values
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 keep persistence/event semantics

Provider Wiring

Core providers live under lib/ui/core/providers/.
  • repository_providers.dart: repository instances
  • logger_provider.dart: shared logger
  • task_actions_providers.dart
  • check_in_actions_providers.dart
  • event_actions_providers.dart
  • mana_actions_providers.dart
There is one provider per action class. Feature query providers (for example taskListProvider) also live in ui/core/providers and must call read actions, not repositories directly.

ViewModel Conventions

ViewModels remain AsyncNotifier<void> in this phase.
  • Commands are methods on the ViewModel
  • Commands return Future<ApplicationResult<T>>
  • ViewModels read action providers, not repository providers

Folder Layout

lib/
  data/
    database/
      tables/
    repositories/
  domain/
    application/
      common/
      tasks/
      check_ins/
      events/
    enums/
    models/
    repositories/
    services/
  ui/
    core/
      design/
      providers/
      widgets/
    tasks/
      view_models/
      widgets/
    orchard/
      view_models/
      widgets/
    check_ins/
      view_models/

Naming

ThingConventionExample
Application actionVerb phrase, no suffixCreateTask, GetTaskById
Repository interface{Entity}RepositoryTaskRepository
Drift implementationDrift prefixDriftTaskRepository
ViewModel{Feature}ViewModelTasksViewModel
ViewModel provider{feature}ViewModelProvidertasksViewModelProvider

Async Patterns

  • Reactive reads: StreamProvider + watch actions
  • Point-in-time reads/writes: Future + ApplicationResult<T>
  • No raw Drift query exposure above data adapters