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 providersdomain/: models, repository contracts, application actions, pure mana logicdata/: 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
Test code
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:
watchAllTasksaction ->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.createTaskOrchardViewModel.completeTaskOccurrenceCheckInViewModel.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
AsyncNotifierViewModels- provider composition
domain/application/
Put code here when you need one operation with validation, failure mapping, and logging.
CreateTaskGetTaskByIdWatchComputedManaDay
domain/repositories/
Put abstract contracts here. These are the stable seams the rest of the app programs against.
Examples:
TaskRepositoryCheckInRepositoryEventRepositoryManaRepository
data/repositories/
Put concrete Drift-backed adapters here.
They are responsible for:
- queries
- transactions
- row-to-domain mapping
- event append semantics
domain/mana/
This is the home for pure mana logic.
models/: engine input/output modelsengine/: calculators, detectors, orchestratorssupport/: deterministic helpers such as local-day utilities
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
- Add or update the repository contract in
domain/repositories/ - Implement it in
data/repositories/ - Add a
Get*orWatch*action indomain/application/ - Wire an action provider in
ui/core/providers/ - Expose a feature
FutureProviderorStreamProvider - Read that provider from the screen
New command
- Add the repository write API
- Implement the write in the Drift adapter
- Add a mutation action with validation and logging
- Inject it in the relevant ViewModel
- Call the ViewModel method from the widget
New mana rule
- Add or update the pure engine type in
domain/mana/engine/ - Add deterministic fixtures/builders under
test/domain/mana/support/ - Add subsystem tests first
- 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