Architecture
Karter follows a clean-ish layered architecture with three main layers: domain/, data/, and presentation/. State management is handled by Riverpod, and local persistence by Drift (SQLite).
Overview
Core Entities
Vehicle
The root aggregate of the domain.
Notes:
plateandvinare stored as plainString?, backed by value objects indomain/value_objects/for validation.currencyis a 3-letter ISO code string (USD, ARS, EUR, etc.) used for fuel price and service cost display.fuelVolumeUnitdetermines whether fuel-ups display in liters or gallons.
FuelLog
Records a refueling event to track expenses and calculate consumption.
calculatedConsumptionreturns L/100km or MPG depending on units.- Volume unit is inherited from vehicle settings, not stored per entry.
pricePerUnitis displayed with the vehicle's currency symbol.
MaintenanceLog
Records services or repairs performed on the vehicle.
photoPathsare stored as file paths to images in{appDocDir}/maintenance_photos/{logId}/.costAmount/costCurrencystore historical cost values independent of vehicle's current currency setting.
VehicleDocument
Attaches files (receipts, photos, PDFs) to a vehicle.
- Files are stored at
{appDocDir}/documents/{vehicleId}/{uuid}.{ext}. - Document types are displayed with localized labels and icons.
MaintenanceInterval
Tracks periodic maintenance tasks with km/time thresholds.
i18nKey/descI18nKeymap to JSON keys ini18n/*.jsonfor built-in intervals (oil change, filters, etc.), resolved viaTemplateTranslations.isCustomdistinguishes user-created intervals from seeded defaults.lastResetKm/lastResetDatetrack the last service reset for "next in X km" calculations.
Enums
| Enum | File | Values |
|---|---|---|
VehicleType | domain/enums/vehicle_type.dart | combustion, electric, motorcycle |
DistanceUnit | domain/enums/distance_unit.dart | kilometers, miles |
VolumeUnit | domain/enums/volume_unit.dart | liters, gallons |
DocumentType | domain/enums/document_type.dart | fine, parkingFee, insurance, vehicleCheck, tax, complexInsurance, vehicleRegister, other |
CoreError | domain/enums/core_error.dart | emptyLicensePlate, invalidLicensePlateFormat, negativeOdometer, invalidVehicleYear, invalidVinFormat |
Value Objects
Immutable objects that enforce formatting and prevent primitive obsession.
Odometer
Volume
Plate
Validates license plate format by country code. Currently supports Argentina (ABC·123, AB·123·CD) and generic alphanumeric formats.
VIN
Validates 17-character VIN structure and extracts:
| Position | Section | Description |
|---|---|---|
| 1 - 3 | WMI | World Manufacturer Identifier |
| 4 - 8 | VDS | Vehicle Descriptor Section |
| 9 | VDS | Check Digit |
| 10 | VIS | Model Year |
| 11 | VIS | Plant Code |
| 12 - 17 | VIS | Serial Number |
Error Handling
All business rule violations throw DomainException with a CoreError enum value. Presentation layer catches these to show localized error messages.
Data Services
Template Resolution (data/services/template_resolver.dart)
Fetches template JSON from the configured online template repo (the same repo URL as More → Repo URL). The app's runtime template data actually comes from the catalog database (CatalogRepository); the resolver is used by the template creator page, which always reads the online JSON (index.json + data/**/*.json) instead of the DB.
loadIndex(baseUrl:)fetchesindex.jsonfrom the online repo (falling back to bundled assets when offline).resolveExtendsChain(paths, baseUrl:)walks the fullextendschain of each selected extends path and returns every inherited part/item tagged with its origin file — mirroringbuild_catalog.py's override/removesemantics.- Used by
template_creator_page.dartfor the make/model autocomplete, the computed extends, and the read-only inherited parts/maintenance lists.
Template Translations (data/services/template_translations.dart)
Loads community/template translations (maintenance item names, descriptions, brand names) from flat JSON files in templates/i18n/. These are separate from the ARB-based UI translations in lib/l10n/. Supports remote fetch (same GitHub URL as templates) with local asset fallback.
- Translations are preloaded at app startup in
main(), respecting the template source config (online/offline). - Remote mode: fetches from
{repoUrl}/i18n/{locale}.json, falls back to bundledtemplates/i18n/on failure. maintenance_localizer.dartuses this service to resolve interval names/descriptions.- JSON format is flat key-value (
{"seed_interval_oil_change": "Oil change"}) for easy community contribution. - Three files:
templates/i18n/en.json(English),templates/i18n/es.json(Spanish), andtemplates/i18n/et.json(Estonian). Accessible in the app via a symlink atmobile/i18n/(listed as a separate asset inpubspec.yamlsince Flutter doesn't recursively bundle symlinked subdirectories on Linux).
PDF Export (data/services/pdf_export_service.dart)
Generates maintenance report PDFs using the pdf and printing packages.
- Output: in-memory PDF bytes, shared via
Share.shareXFiles. - Uses Roboto font from
PdfGoogleFonts(bundled withprintingpackage) for Unicode support. - Linux desktop fallback:
xdg-opensinceshare_plusis unimplemented.
Data Export/Import (data/services/export_service.dart)
Exports all vehicle data (vehicles, fuel logs, maintenance logs) as a JSON file for backup or transfer.
Persistence
Drift Database (core/database/app_database.dart)
| Table | Schema Version Added | Notes |
|---|---|---|
Vehicles | v1 | Has fuelVolumeUnit (v9), currency (v10) |
FuelLogs | v1 | |
MaintenanceLogs | v1 | Has photoPaths, costAmount, costCurrency (v10) |
MaintenanceIntervals | v1 | |
VehicleDocuments | v7 |
Current schema version: 10
Migrations are handled incrementally in MigrationStrategy inside app_database.dart. Each schema change adds a migrate.fromCallback step.
Providers (Riverpod)
All providers are defined in presentation/providers/vehicle_providers.dart and locale_provider.dart.
Repository Providers (singletons)
| Provider | Type | Returns |
|---|---|---|
appDatabaseProvider | Provider<AppDatabase> | Database instance |
vehicleRepositoryProvider | Provider<VehicleRepository> | VehicleRepositoryImpl |
fuelLogRepositoryProvider | Provider<FuelLogRepository> | FuelLogRepositoryImpl |
maintenanceLogRepositoryProvider | Provider<MaintenanceLogRepository> | MaintenanceLogRepositoryImpl |
maintenanceIntervalRepositoryProvider | Provider<MaintenanceIntervalRepository> | MaintenanceIntervalRepositoryImpl |
vehicleDocumentRepositoryProvider | Provider<VehicleDocumentRepository> | VehicleDocumentRepositoryImpl |
exportServiceProvider | Provider<ExportService> | ExportService |
templateResolverProvider | Provider<TemplateResolver> | TemplateResolver |
catalogServiceProvider | Provider<CatalogService> | Catalog download/refresh/import service |
catalogRepositoryProvider | Provider<CatalogRepository> | Reads the active catalog DB (index, best match, DTCs) |
templateSourceProvider | NotifierProvider<TemplateSourceConfig> | Online template repo URL + version (More → Repo URL) |
catalogSourcesProvider | NotifierProvider<CatalogSourcesState> | Active catalog source (builtin / online / local) |
onlineTemplateBaseUrlProvider | FutureProvider<String?> | Resolved raw base URL of the template repo |
onlineTemplateIndexProvider | FutureProvider<TemplateIndex> | index.json fetched from the online repo |
pdfExportServiceProvider | Provider<PdfExportService> | PdfExportService |
Data Providers (async, family by vehicleId)
| Provider | Returns |
|---|---|
vehicleListProvider | List<Vehicle> |
vehicleProvider(vehicleId) | Vehicle? |
fuelLogsProvider(vehicleId) | List<FuelLog> |
maintenanceLogsProvider(vehicleId) | List<MaintenanceLog> |
maintenanceIntervalsProvider(vehicleId) | List<MaintenanceInterval> |
vehicleDocumentsProvider(vehicleId) | List<VehicleDocument> |
Locale Provider
| Provider | Purpose |
|---|---|
localeProvider | Read/write current locale (en/es), persisted in SharedPreferences |
Data Flow Example
User taps "Add fuel log"
→ vehicle_detail_page calls showAddFuelLogModal(context, vehicleId)
→ Modal reads vehicle from ref (for odometer, volume unit, currency)
→ User fills form, taps "Save"
→ Modal calls fuelLogRepositoryProvider.save(FuelLog(...))
→ Repository impl converts to Drift companion, inserts row
→ Modal pops with result=true
→ Vehicle detail page invalidates fuelLogsProvider to refresh UI
- There is no use-case/orchestrator layer — providers call repositories directly.
- Modals return
boolto signal success; the caller invalidates the relevant provider.