From d2ab99867eddc95fe6c7cc99d8af3e8286b5f0f3 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 11 Sep 2026 13:52:33 +0200 Subject: [PATCH] feat(di): Hilt modules for the database, repositories and clocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The database and its DAOs, the four repository bindings, and the Android implementations of the two clocks โ€” so nothing constructs a Room database or reads a system clock by hand. Co-Authored-By: Claude Opus 5 --- .../clockula/data/di/DataModule.kt | 4 +- .../clockula/data/di/DatabaseModule.kt | 42 +++++++++++++++++++ .../clockula/data/di/RepositoryModule.kt | 37 ++++++++++++++++ .../clockula/data/di/TimeModule.kt | 25 +++++++++++ .../clockula/data/time/AndroidClocks.kt | 23 ++++++++++ 5 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/data/di/DatabaseModule.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/data/di/RepositoryModule.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/data/di/TimeModule.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClocks.kt diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/di/DataModule.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/di/DataModule.kt index eaeca73..dcbd3c6 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/di/DataModule.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/di/DataModule.kt @@ -21,8 +21,8 @@ import java.io.File import javax.inject.Singleton /** - * Clockula's storage bindings. M1 wires the preference half only; the Room - * database and its DAOs join this module in M2. + * The DataStore half of Clockula's storage. The Room database and its DAOs live + * next door in `DatabaseModule`. */ @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/di/DatabaseModule.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/di/DatabaseModule.kt new file mode 100644 index 0000000..c2f1ed7 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/di/DatabaseModule.kt @@ -0,0 +1,42 @@ +package de.jeanlucmakiola.clockula.data.di + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import de.jeanlucmakiola.clockula.data.alarms.AlarmDao +import de.jeanlucmakiola.clockula.data.db.ClockulaDatabase +import de.jeanlucmakiola.clockula.data.stopwatch.LapDao +import de.jeanlucmakiola.clockula.data.timers.TimerDao +import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockDao +import javax.inject.Singleton + +/** + * The database and its four DAOs. No destructive-migration fallback: docs/PLAN.md + * ยง12 wants every schema change to arrive with a tested migration, and a fallback + * would quietly drop a user's alarms instead of failing loudly in review. + */ +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): ClockulaDatabase = + Room.databaseBuilder(context, ClockulaDatabase::class.java, ClockulaDatabase.NAME).build() + + @Provides + fun provideAlarmDao(database: ClockulaDatabase): AlarmDao = database.alarmDao() + + @Provides + fun provideTimerDao(database: ClockulaDatabase): TimerDao = database.timerDao() + + @Provides + fun provideWorldClockDao(database: ClockulaDatabase): WorldClockDao = database.worldClockDao() + + @Provides + fun provideLapDao(database: ClockulaDatabase): LapDao = database.lapDao() +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/di/RepositoryModule.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/di/RepositoryModule.kt new file mode 100644 index 0000000..aa12c42 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/di/RepositoryModule.kt @@ -0,0 +1,37 @@ +package de.jeanlucmakiola.clockula.data.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import de.jeanlucmakiola.clockula.data.alarms.AlarmRepository +import de.jeanlucmakiola.clockula.data.alarms.AlarmRepositoryImpl +import de.jeanlucmakiola.clockula.data.stopwatch.StopwatchRepository +import de.jeanlucmakiola.clockula.data.stopwatch.StopwatchRepositoryImpl +import de.jeanlucmakiola.clockula.data.timers.TimerRepository +import de.jeanlucmakiola.clockula.data.timers.TimerRepositoryImpl +import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockRepository +import de.jeanlucmakiola.clockula.data.worldclocks.WorldClockRepositoryImpl +import javax.inject.Singleton + +/** The four interfaces the rest of the app sees โ€” and the only ones it may see. */ +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindAlarmRepository(impl: AlarmRepositoryImpl): AlarmRepository + + @Binds + @Singleton + abstract fun bindTimerRepository(impl: TimerRepositoryImpl): TimerRepository + + @Binds + @Singleton + abstract fun bindWorldClockRepository(impl: WorldClockRepositoryImpl): WorldClockRepository + + @Binds + @Singleton + abstract fun bindStopwatchRepository(impl: StopwatchRepositoryImpl): StopwatchRepository +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/di/TimeModule.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/di/TimeModule.kt new file mode 100644 index 0000000..08effc3 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/di/TimeModule.kt @@ -0,0 +1,25 @@ +package de.jeanlucmakiola.clockula.data.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import de.jeanlucmakiola.clockula.data.time.SystemElapsedRealtimeClock +import de.jeanlucmakiola.clockula.data.time.SystemWallClock +import de.jeanlucmakiola.clockula.domain.time.ElapsedRealtimeClock +import de.jeanlucmakiola.clockula.domain.time.WallClock + +/** + * Both clocks are injected, never called statically, so the wall-clock / + * elapsed-realtime distinction can be tested from both sides. + */ +@Module +@InstallIn(SingletonComponent::class) +abstract class TimeModule { + + @Binds + abstract fun bindWallClock(impl: SystemWallClock): WallClock + + @Binds + abstract fun bindElapsedRealtimeClock(impl: SystemElapsedRealtimeClock): ElapsedRealtimeClock +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClocks.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClocks.kt new file mode 100644 index 0000000..1cb442e --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/time/AndroidClocks.kt @@ -0,0 +1,23 @@ +package de.jeanlucmakiola.clockula.data.time + +import android.os.SystemClock +import de.jeanlucmakiola.clockula.domain.time.ElapsedRealtimeClock +import de.jeanlucmakiola.clockula.domain.time.WallClock +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Instant +import javax.inject.Inject + +/** The device's calendar time. Moves with the user, the network and DST. */ +class SystemWallClock @Inject constructor() : WallClock { + override fun now(): Instant = Instant.fromEpochMilliseconds(System.currentTimeMillis()) +} + +/** + * Milliseconds since boot, including deep sleep. Never decreases within a boot + * and no setting can move it โ€” which is exactly why a running timer and the + * stopwatch anchor on it. + */ +class SystemElapsedRealtimeClock @Inject constructor() : ElapsedRealtimeClock { + override fun elapsedRealtime(): Duration = SystemClock.elapsedRealtime().milliseconds +}