Architecture topic
Architecture
Dutato follows Clean Architecture with a Flutter twist: there is no repository layer. Datasources inject directly into Riverpod providers, which feed screens and widgets.
Layer rules
lib/
├── domain/ Pure Dart; zero dependencies outside dart:core.
│ Contains entities only — no I/O, no Flutter.
│
├── data/ Talks to the outside world.
│ ├── models/ freezed + json_serializable DTOs
│ ├── datasources/ Supabase, Claude, Gemini, OpenAI, ONNX, SQLite
│ └── services/ Cross-cutting: notifications, chunk sync, retrieval
│
├── presentation/ Flutter UI + state.
│ ├── providers/ Riverpod (@riverpod code generation)
│ ├── screens/ One directory per feature area
│ └── widgets/ Reusable UI (web_content_frame, role_gate, …)
│
├── config/ Routes, theme, Supabase/env/AI/retrieval config
└── core/ Utils (SM-2, FSRS), extensions, error mapping
Dependency arrows only point inward. presentation/ depends on data/ and domain/. data/ depends on domain/. domain/ depends on nothing.
Code generation
Three generators run together via build_runner:
freezed→ immutable models withcopyWith,==,toString. Produces*.freezed.dart.json_serializable→fromJson/toJson. Produces*.g.dart(alongside freezed part files).riverpod_generator→ provider scaffolding from@riverpodannotations. Produces*.g.dart.
All three run with one command:
dart run build_runner build --delete-conflicting-outputs
Dartdoc needs this to have run — it resolves generated references at doc-build time.
Web vs native: conditional-import barrels
Three native-only dependencies are web-incompatible. Each one hides behind a *_barrel.dart that chooses the real implementation on iOS/Android and a no-op stub on web:
| Module | Native | Web |
|---|---|---|
| ONNX embeddings | local_embedding_datasource.dart |
local_embedding_datasource_stub.dart |
| SQLite vector store | local_vector_store.dart |
local_vector_store_stub.dart |
| Local notifications | notification_service.dart |
notification_service_stub.dart |
Always import via the barrel, never the direct file. Providers for these services are nullable (Provider<X?>) and return null on web via kIsWeb guards. The ContentRetriever cascade falls back to Supabase FTS when local services are null.
Error handling
core/errors/error_mapper.dart exposes userFriendlyMessage(Object) which maps AuthException, PostgrestException, AiApiException, DioException, StateError to user-facing strings. Never surface e.toString() in the UI — always go through the mapper.
Functions
-
userFriendlyMessage(
Object error, {String? logName}) → String Architecture - Maps raw exceptions to user-friendly messages for display in the UI.