feat(di): Hilt modules for the database, repositories and clocks

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 13:52:33 +02:00
co-authored by Claude Opus 5
parent 6b7a4d73ed
commit d2ab99867e
5 changed files with 129 additions and 2 deletions
@@ -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)
@@ -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()
}
@@ -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
}
@@ -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
}
@@ -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
}