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 with copyWith, ==, toString. Produces *.freezed.dart.
  • json_serializablefromJson / toJson. Produces *.g.dart (alongside freezed part files).
  • riverpod_generator → provider scaffolding from @riverpod annotations. 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.