Skip to main content

Overview

The app follows one strict direction of flow: UI -> ViewModel -> Application Action -> Repository -> Drift That split exists to keep widgets simple, keep business logic deterministic, and make the data layer replaceable without rewriting the UI. The main layers in app/lib/ are:
  • ui/: screens, widgets, ViewModels, Riverpod providers
  • domain/: models, repository contracts, application actions, pure mana logic
  • data/: Drift tables, database setup, Drift-backed repository adapters
The domain layer is the dependency target. It does not import from ui/ or data/.

Directory structure

Production code

app/lib/
  data/
    database/
      app_database.dart
      tables/
    repositories/
  domain/
    application/
      common/
      check_ins/
      events/
      mana/
      tasks/
    enums/
    mana/
      engine/
      models/
      support/
    models/
    repositories/
    services/
  ui/
    check_ins/
      view_models/
    core/
      design/
      providers/
      view_models/
      widgets/
    mana/
      view_models/
      widgets/
    orchard/
      view_models/
      widgets/
    tasks/
      view_models/
      widgets/

Test code

app/test/
  data/
  domain/
    application/
      check_ins/
      events/
      mana/
      tasks/
    mana/
      engine/
      models/
      support/
  ui/
    check_ins/
      view_models/
    core/
      providers/
    orchard/
      view_models/
    tasks/
      view_models/

How the pieces fit together

Read path

A read should go through a provider and an action, not directly from a widget to a repository. Examples:
  • task list: watchAllTasks action -> TaskRepository -> DriftTaskRepository
  • check-in window: GetCheckInsInWindow -> CheckInRepository -> DriftCheckInRepository
  • mana inputs: GetManaDayInputs / WatchManaDayInputs -> ManaRepository -> DriftManaRepository

Command path

Commands originate in ViewModels so widgets stay thin and testable. Examples:
  • TasksViewModel.createTask
  • OrchardViewModel.completeTaskOccurrence
  • CheckInViewModel.saveCheckIn

Mana computation path

Mana is split into input assembly and pure computation. This keeps SQL and replay concerns out of the mana engine itself.

Provider composition

ui/core/providers/ is the composition root for the app. Rules:
  • repository providers construct concrete Drift adapters
  • action providers construct one action per operation
  • query providers call actions, not repositories directly
  • ViewModels call command actions, not Drift adapters

What belongs where

ui/

Put code here when it is about rendering, interaction, or feature-scoped state wiring.
  • widgets
  • screens
  • AsyncNotifier ViewModels
  • provider composition
Do not put SQL, repository implementations, or mana formulas here.

domain/application/

Put code here when you need one operation with validation, failure mapping, and logging.
  • CreateTask
  • GetTaskById
  • WatchComputedManaDay
Each action should do orchestration, not persistence details.

domain/repositories/

Put abstract contracts here. These are the stable seams the rest of the app programs against. Examples:
  • TaskRepository
  • CheckInRepository
  • EventRepository
  • ManaRepository

data/repositories/

Put concrete Drift-backed adapters here. They are responsible for:
  • queries
  • transactions
  • row-to-domain mapping
  • event append semantics
They are not responsible for UI concerns or mana formulas.

domain/mana/

This is the home for pure mana logic.
  • models/: engine input/output models
  • engine/: calculators, detectors, orchestrators
  • support/: deterministic helpers such as local-day utilities
This layer should stay replayable and storage-agnostic.

Dependency rules

Practical consequences:
  • domain models must not import Drift row classes
  • widgets must not run core calculations
  • data adapters may map database rows to domain models, but not the other way around

How to add new code

New read feature

  1. Add or update the repository contract in domain/repositories/
  2. Implement it in data/repositories/
  3. Add a Get* or Watch* action in domain/application/
  4. Wire an action provider in ui/core/providers/
  5. Expose a feature FutureProvider or StreamProvider
  6. Read that provider from the screen

New command

  1. Add the repository write API
  2. Implement the write in the Drift adapter
  3. Add a mutation action with validation and logging
  4. Inject it in the relevant ViewModel
  5. Call the ViewModel method from the widget

New mana rule

  1. Add or update the pure engine type in domain/mana/engine/
  2. Add deterministic fixtures/builders under test/domain/mana/support/
  3. Add subsystem tests first
  4. Wire the engine through the mana actions/providers only after the domain contract is stable

Testing strategy

Tests mirror the production layers. This split keeps failures local:
  • a Drift regression should fail in test/data
  • an action contract regression should fail in test/domain/application
  • a mana formula regression should fail in test/domain/mana
  • a provider or ViewModel wiring regression should fail in test/ui

Reference pages