Architecture Overview¶
CyclingAssistant follows a multi-module Clean Architecture approach with MVVM pattern and unidirectional data flow. Features are isolated into independent modules that communicate through the bridge pattern.
Layer Dependencies¶
Each feature module follows strict unidirectional dependencies across three layers:
- presentation — Composables, ViewModels, UI state
- domain — Use cases, repository interfaces, business logic (no Android dependencies)
- data — Repository implementations, data sources, mappers
Composable (Route/Content)
│ observes
▼
ViewModel
│ calls
▼
UseCase
│ calls
▼
Repository
│ calls
▼
DataSource
Dependency Rules
- Upper layers depend on lower layers, never the reverse
- ViewModel never accesses Repository or DataSource directly — always through UseCase
- DataSource never accesses UseCase or ViewModel
Interface + Impl Pattern¶
UseCases, DataSources, Mappers, and Repositories all follow the Interface + Impl pattern:
- UseCase: interface and
Implare colocated in the same file, along with related custom exceptions - Mapper / DataSource: interfaces may use separate files for interface and impl
Error Handling¶
The project uses suspendRunCatching (from shared/concurrent) instead of Kotlin's standard runCatching in coroutine contexts. It rethrows CancellationException to preserve structured concurrency.
- Repository suspend functions return
Result<T> - Repository observable functions return
Flow<T>(noResultwrapping)
Dependency Injection (Hilt)¶
All dependencies are managed via Hilt. Registration conventions:
| Type | Hilt Pattern |
|---|---|
| UseCase | @Provides (no scope) for stateless; @Provides @Singleton for stateful |
| DataSource | @Provides @Singleton |
| Mapper | @Provides @Singleton |
| Repository | @Provides @Singleton |
| ViewModel | @HiltViewModel internal class ... @Inject constructor(...) |
Qualifiers¶
Defined in shared/di as @Qualifier @Retention(AnnotationRetention.BINARY) annotations
(e.g., @IoDispatcher, @DefaultDispatcher, @SessionMutex, @SessionDaoFactory).
DI File Organization¶
Each feature module organizes DI into separate files:
<Feature>DataHiltModule.kt—@Module @InstallIn(SingletonComponent::class) internal object<Feature>DomainHiltModule.kt—@Module @InstallIn(SingletonComponent::class) internal object<Feature>PresentationHiltModule.kt— only if non-VM bindings exist (error mappers, factories)- No aggregator needed — Hilt auto-discovers
@Moduleclasses
Composable Injection¶
- Use
hiltViewModel()for ViewModels - For non-VM dependencies in Composables, use
@EntryPoint @InstallIn(SingletonComponent::class)interfaces withEntryPointAccessors.fromApplication()
Dispatcher Usage¶
Dispatchers are injected via @IoDispatcher/@DefaultDispatcher/@MainDispatcher qualifiers, never hardcoded:
| Layer | Dispatcher |
|---|---|
| ViewModel | dispatcherDefault |
| Mapper | dispatcherDefault |
| Repository | delegates (or dispatcherDefault) |
| DataSource | dispatcherIo |
Database¶
Centralized Room database in the app module (AppDatabase):
DestinationDao— cycling POI dataLocaleDao— language/locale settingsPairedDeviceDao— paired BLE devicesProfileDao— rider profile dataSessionDao— session and track points
@Dao interfaces use suspend functions for one-shot operations and Flow for observable queries. Default conflict strategy: OnConflictStrategy.REPLACE.
Data Persistence¶
| Storage | Purpose |
|---|---|
| Room | Destinations, sessions, track points, locale, profile, paired devices |
| DataStore | User preferences (theme, language, profile, stats display configuration) |