From 65970d45d25750a36e227fa2b15f746d03a11613 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 09:19:27 +0000 Subject: [PATCH 1/2] Migrate dependency injection from Kodein to Koin Replace Kodein DI framework with Koin 4.0.1 across the entire codebase. This migration modernizes the DI approach and aligns with current Kotlin Multiplatform best practices. Changes: - Update gradle dependencies: Replace kodein-di with koin-core, koin-compose, koin-compose-viewmodel, and koin-android - Migrate all DI modules to Koin syntax: - Replace DI.Module() with module {} - Replace bind() with singleton with single {} - Replace bind() with provider with factory {} - Replace instance() with get() - Replace importAll() with includes() - Update PlatformSDK to use Koin API: - Replace DirectDI with Koin - Replace DI.direct with startKoin().koin - Update platform-specific providers for Android, iOS, and JVM - Maintain backward compatibility with existing injection points through the Inject helper object All existing ViewModels and use cases continue to work without changes through the maintained Inject.instance() API. Co-Authored-By: Claude Sonnet 4.5 --- composeApp/build.gradle.kts | 5 ++- .../src/androidMain/kotlin/di/Providers.kt | 12 +++--- .../src/commonMain/kotlin/di/CoreModule.kt | 6 +-- .../commonMain/kotlin/di/DatabaseModule.kt | 32 +++++++-------- .../src/commonMain/kotlin/di/FeatureModule.kt | 39 +++++++++---------- .../src/commonMain/kotlin/di/PlatformSDK.kt | 36 ++++++++--------- .../src/commonMain/kotlin/di/Providers.kt | 4 +- .../kotlin/di/SerializationModule.kt | 8 ++-- .../kotlin/feature/daily/di/DailyModule.kt | 26 ++++++------- .../kotlin/feature/detail/di/DetailModule.kt | 20 +++++----- .../kotlin/feature/habits/di/HabitModule.kt | 14 +++---- .../feature/projects/di/ProjectModule.kt | 15 +++---- .../feature/settings/di/SettingsModule.kt | 11 ++---- .../feature/tracker/di/TrackerModule.kt | 14 +++---- .../src/iosMain/kotlin/di/Providers.ios.kt | 8 ++-- .../jvmMain/kotlin/di/Providers.desktop.kt | 8 ++-- gradle/libs.versions.toml | 6 ++- 17 files changed, 123 insertions(+), 141 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 8574574..7a9c21a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -83,7 +83,9 @@ kotlin { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.datetime) - implementation(libs.kodein.di) + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) implementation(libs.uuid) @@ -103,6 +105,7 @@ kotlin { androidMain.dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.activity.compose) + implementation(libs.koin.android) } jvmMain.dependencies { diff --git a/composeApp/src/androidMain/kotlin/di/Providers.kt b/composeApp/src/androidMain/kotlin/di/Providers.kt index 1a95738..491abba 100644 --- a/composeApp/src/androidMain/kotlin/di/Providers.kt +++ b/composeApp/src/androidMain/kotlin/di/Providers.kt @@ -1,13 +1,11 @@ package di import core.platform.ImagePicker -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.instance -import org.kodein.di.singleton +import org.koin.core.module.Module +import org.koin.dsl.module -actual fun DI.Builder.provideImagePicker() { - bind() with singleton { - instance().imagePicker +actual fun Module.provideImagePicker() { + single { + get().imagePicker } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/di/CoreModule.kt b/composeApp/src/commonMain/kotlin/di/CoreModule.kt index 09364d7..ebd6a47 100644 --- a/composeApp/src/commonMain/kotlin/di/CoreModule.kt +++ b/composeApp/src/commonMain/kotlin/di/CoreModule.kt @@ -1,9 +1,9 @@ package di -import org.kodein.di.DI +import org.koin.dsl.module -val coreModule = DI.Module("coreModule") { - importAll( +val coreModule = module { + includes( serializationModule ) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/di/DatabaseModule.kt b/composeApp/src/commonMain/kotlin/di/DatabaseModule.kt index 8be5211..7bef409 100644 --- a/composeApp/src/commonMain/kotlin/di/DatabaseModule.kt +++ b/composeApp/src/commonMain/kotlin/di/DatabaseModule.kt @@ -6,33 +6,31 @@ import feature.daily.data.DailyDao import feature.habits.data.HabitDao import feature.projects.data.ProjectDao import feature.tracker.data.TrackerDao -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.instance -import org.kodein.di.singleton +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module -fun databaseModule() = DI.Module("database") { - bind() with singleton { - instance("appDatabase") as AppDatabase +val databaseModule = module { + single { + get(qualifier = org.koin.core.qualifier.named("appDatabase")) } - bind() with singleton { - instance().getHabitDao() + single { + get().getHabitDao() } - bind() with singleton { - instance().getTrackerDao() + single { + get().getTrackerDao() } - bind() with singleton { - instance().getDailyDao() + single { + get().getDailyDao() } - bind() with singleton { - instance().getUserProfileDao() + single { + get().getUserProfileDao() } - bind() with singleton { - instance().getProjectDao() + single { + get().getProjectDao() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/di/FeatureModule.kt b/composeApp/src/commonMain/kotlin/di/FeatureModule.kt index c9fcc83..9bef373 100644 --- a/composeApp/src/commonMain/kotlin/di/FeatureModule.kt +++ b/composeApp/src/commonMain/kotlin/di/FeatureModule.kt @@ -7,45 +7,42 @@ import feature.habits.domain.CreateHabitUseCase import feature.projects.di.projectModule import feature.settings.domain.ClearAllHabitsUseCase import feature.tracker.domain.UpdateTrackerValueUseCase -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.instance -import org.kodein.di.singleton +import org.koin.dsl.module -fun featureModule() = DI.Module("feature") { - importAll( +val featureModule = module { + includes( detailModule, projectModule ) - + // Use Cases - bind() with singleton { + single { GetHabitsForTodayUseCase( - habitDao = instance(), - trackerDao = instance(), - dailyDao = instance() + habitDao = get(), + trackerDao = get(), + dailyDao = get() ) } - bind() with singleton { + single { SwitchHabitUseCase( - habitDao = instance(), - dailyDao = instance() + habitDao = get(), + dailyDao = get() ) } - bind() with singleton { + single { CreateHabitUseCase( - habitDao = instance() + habitDao = get() ) } - bind() with singleton { + single { UpdateTrackerValueUseCase( - trackerDao = instance() + trackerDao = get() ) } - bind() with singleton { + single { ClearAllHabitsUseCase( - habitDao = instance(), - dailyDao = instance() + habitDao = get(), + dailyDao = get() ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/di/PlatformSDK.kt b/composeApp/src/commonMain/kotlin/di/PlatformSDK.kt index 75e4f4b..395b1b9 100644 --- a/composeApp/src/commonMain/kotlin/di/PlatformSDK.kt +++ b/composeApp/src/commonMain/kotlin/di/PlatformSDK.kt @@ -1,43 +1,41 @@ package di -import org.kodein.di.DI -import org.kodein.di.DirectDI -import org.kodein.di.bind -import org.kodein.di.direct -import org.kodein.di.instance -import org.kodein.di.singleton +import org.koin.core.Koin +import org.koin.core.context.startKoin +import org.koin.core.qualifier.named +import org.koin.dsl.module object PlatformSDK { - private var _di: DirectDI? = null - val di: DirectDI - get() = requireNotNull(_di) + private var _koin: Koin? = null + val koin: Koin + get() = requireNotNull(_koin) fun init( configuration: PlatformConfiguration, appDatabase: Any? = null ) { - val configModule = DI.Module("config") { - bind() with singleton { configuration } + val configModule = module { + single { configuration } if (appDatabase != null) { - bind("appDatabase") with singleton { appDatabase } + single(qualifier = named("appDatabase")) { appDatabase } } } - val platformModule = DI.Module("platform") { + val platformModule = module { provideImagePicker() } - _di = DI { - importAll( + _koin = startKoin { + modules( configModule, platformModule, - databaseModule(), - featureModule() + databaseModule, + featureModule ) - }.direct + }.koin } inline fun instance(): T { - return di.instance() + return koin.get() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/di/Providers.kt b/composeApp/src/commonMain/kotlin/di/Providers.kt index d7a88b8..eff564e 100644 --- a/composeApp/src/commonMain/kotlin/di/Providers.kt +++ b/composeApp/src/commonMain/kotlin/di/Providers.kt @@ -1,5 +1,5 @@ package di -import org.kodein.di.DI +import org.koin.core.module.Module -expect fun DI.Builder.provideImagePicker() \ No newline at end of file +expect fun Module.provideImagePicker() \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/di/SerializationModule.kt b/composeApp/src/commonMain/kotlin/di/SerializationModule.kt index 2643424..e54469c 100644 --- a/composeApp/src/commonMain/kotlin/di/SerializationModule.kt +++ b/composeApp/src/commonMain/kotlin/di/SerializationModule.kt @@ -1,12 +1,10 @@ package di import kotlinx.serialization.json.Json -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.singleton +import org.koin.dsl.module -val serializationModule = DI.Module("serializationModule") { - bind() with singleton { +val serializationModule = module { + single { Json { isLenient = true ignoreUnknownKeys = true diff --git a/composeApp/src/commonMain/kotlin/feature/daily/di/DailyModule.kt b/composeApp/src/commonMain/kotlin/feature/daily/di/DailyModule.kt index f01b8e4..4834eb9 100644 --- a/composeApp/src/commonMain/kotlin/feature/daily/di/DailyModule.kt +++ b/composeApp/src/commonMain/kotlin/feature/daily/di/DailyModule.kt @@ -2,30 +2,26 @@ package feature.daily.di import core.database.AppDatabase import data.features.daily.DailyRepository -import di.Inject.instance import feature.daily.data.DailyDao import feature.daily.domain.GetHabitsForTodayUseCase import feature.daily.domain.SwitchHabitUseCase -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.provider -import org.kodein.di.singleton +import org.koin.dsl.module -val dailyModule = DI.Module("DailyModule") { - bind() with singleton { - val appDatabase = instance() +val dailyModule = module { + single { + val appDatabase = get() appDatabase.getDailyDao() } - - bind() with provider { - GetHabitsForTodayUseCase(instance(), instance(), instance()) + + factory { + GetHabitsForTodayUseCase(get(), get(), get()) } - - bind() with provider { - SwitchHabitUseCase(instance(), instance()) + + factory { + SwitchHabitUseCase(get(), get()) } - bind() with provider { + factory { DailyRepository() } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/feature/detail/di/DetailModule.kt b/composeApp/src/commonMain/kotlin/feature/detail/di/DetailModule.kt index 5776220..852a68a 100644 --- a/composeApp/src/commonMain/kotlin/feature/detail/di/DetailModule.kt +++ b/composeApp/src/commonMain/kotlin/feature/detail/di/DetailModule.kt @@ -3,18 +3,18 @@ package feature.detail.di import feature.detail.domain.DeleteHabitUseCase import feature.detail.domain.GetDetailInfoUseCase import feature.detail.domain.UpdateHabitUseCase -import org.kodein.di.* +import org.koin.dsl.module -val detailModule = DI.Module("detailModule") { - bind() with provider { - GetDetailInfoUseCase(instance()) +val detailModule = module { + factory { + GetDetailInfoUseCase(get()) } - - bind() with provider { - DeleteHabitUseCase(instance()) + + factory { + DeleteHabitUseCase(get()) } - - bind() with provider { - UpdateHabitUseCase(instance()) + + factory { + UpdateHabitUseCase(get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/feature/habits/di/HabitModule.kt b/composeApp/src/commonMain/kotlin/feature/habits/di/HabitModule.kt index 03fae4b..bc3ed9a 100644 --- a/composeApp/src/commonMain/kotlin/feature/habits/di/HabitModule.kt +++ b/composeApp/src/commonMain/kotlin/feature/habits/di/HabitModule.kt @@ -3,15 +3,15 @@ package feature.habits.di import core.database.AppDatabase import feature.habits.data.HabitDao import feature.habits.domain.CreateHabitUseCase -import org.kodein.di.* +import org.koin.dsl.module -val habitModule = DI.Module("HabitModule") { - bind() with singleton { - val appDatabase = instance() +val habitModule = module { + single { + val appDatabase = get() appDatabase.getHabitDao() } - - bind() with provider { - CreateHabitUseCase(instance()) + + factory { + CreateHabitUseCase(get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/feature/projects/di/ProjectModule.kt b/composeApp/src/commonMain/kotlin/feature/projects/di/ProjectModule.kt index 898ac43..161a2a5 100644 --- a/composeApp/src/commonMain/kotlin/feature/projects/di/ProjectModule.kt +++ b/composeApp/src/commonMain/kotlin/feature/projects/di/ProjectModule.kt @@ -4,14 +4,11 @@ import feature.projects.domain.CreateProjectUseCase import feature.projects.domain.DeleteProjectUseCase import feature.projects.domain.GetAllProjectsUseCase import feature.projects.domain.UpdateProjectUseCase -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.instance -import org.kodein.di.provider +import org.koin.dsl.module -val projectModule = DI.Module("projectModule") { - bind() with provider { CreateProjectUseCase(instance()) } - bind() with provider { GetAllProjectsUseCase(instance()) } - bind() with provider { UpdateProjectUseCase(instance()) } - bind() with provider { DeleteProjectUseCase(instance(), instance()) } +val projectModule = module { + factory { CreateProjectUseCase(get()) } + factory { GetAllProjectsUseCase(get()) } + factory { UpdateProjectUseCase(get()) } + factory { DeleteProjectUseCase(get(), get()) } } diff --git a/composeApp/src/commonMain/kotlin/feature/settings/di/SettingsModule.kt b/composeApp/src/commonMain/kotlin/feature/settings/di/SettingsModule.kt index 8d5288a..f1500f1 100644 --- a/composeApp/src/commonMain/kotlin/feature/settings/di/SettingsModule.kt +++ b/composeApp/src/commonMain/kotlin/feature/settings/di/SettingsModule.kt @@ -1,13 +1,10 @@ package feature.settings.di -import di.Inject.instance import feature.settings.domain.ClearAllHabitsUseCase -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.provider +import org.koin.dsl.module -val settingsModule = DI.Module("SettingsModule") { - bind() with provider { - ClearAllHabitsUseCase(instance(), instance()) +val settingsModule = module { + factory { + ClearAllHabitsUseCase(get(), get()) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/feature/tracker/di/TrackerModule.kt b/composeApp/src/commonMain/kotlin/feature/tracker/di/TrackerModule.kt index 6ad1d31..972a295 100644 --- a/composeApp/src/commonMain/kotlin/feature/tracker/di/TrackerModule.kt +++ b/composeApp/src/commonMain/kotlin/feature/tracker/di/TrackerModule.kt @@ -3,15 +3,15 @@ package feature.tracker.di import core.database.AppDatabase import feature.tracker.data.TrackerDao import feature.tracker.domain.UpdateTrackerValueUseCase -import org.kodein.di.* +import org.koin.dsl.module -val trackerModule = DI.Module("TrackerModule") { - bind() with singleton { - val appDatabase = instance() +val trackerModule = module { + single { + val appDatabase = get() appDatabase.getTrackerDao() } - - bind() with provider { - UpdateTrackerValueUseCase(instance()) + + factory { + UpdateTrackerValueUseCase(get()) } } \ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/di/Providers.ios.kt b/composeApp/src/iosMain/kotlin/di/Providers.ios.kt index b5c788f..88635c3 100644 --- a/composeApp/src/iosMain/kotlin/di/Providers.ios.kt +++ b/composeApp/src/iosMain/kotlin/di/Providers.ios.kt @@ -2,10 +2,8 @@ package di import core.platform.IOSImagePicker import core.platform.ImagePicker -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.singleton +import org.koin.core.module.Module -actual fun DI.Builder.provideImagePicker(platform: Platform) { - bind() with singleton { IOSImagePicker() } +actual fun Module.provideImagePicker() { + single { IOSImagePicker() } } \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/di/Providers.desktop.kt b/composeApp/src/jvmMain/kotlin/di/Providers.desktop.kt index 08770d6..091ede1 100644 --- a/composeApp/src/jvmMain/kotlin/di/Providers.desktop.kt +++ b/composeApp/src/jvmMain/kotlin/di/Providers.desktop.kt @@ -2,10 +2,8 @@ package di import core.platform.DesktopImagePicker import core.platform.ImagePicker -import org.kodein.di.DI -import org.kodein.di.bind -import org.kodein.di.singleton +import org.koin.core.module.Module -actual fun DI.Builder.provideImagePicker(platform: Platform) { - bind() with singleton { DesktopImagePicker() } +actual fun Module.provideImagePicker() { + single { DesktopImagePicker() } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d836976..68e1886 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,6 +9,7 @@ coroutines = "1.8.1" serialization = "1.6.3" ktor = "2.3.9" klock = "3.4.0" +koin = "4.0.1" # Libraries room = "2.7.0-alpha03" @@ -56,7 +57,10 @@ coil-multiplatform-compose = { module = "io.coil-kt.coil3:coil-compose", version #coil-multiplatform-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil-multiplatform" } coil-multiplatform-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil-multiplatform" } -kodein-di = "org.kodein.di:kodein-di:7.20.2" +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } uuid = "app.softwork:kotlinx-uuid-core:0.0.25" compose-viewmodel = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0" From 25fd12df4603eceeb4d43180fd47dc6e84374ac9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 09:21:36 +0000 Subject: [PATCH 2/2] Fix: Update documentation and config for Koin migration - Update ProGuard rules to use Koin instead of Kodein - Update README.md tech stack to reference Koin - Update IMPLEMENTATION_STATUS.md documentation - Remove unused import in DatabaseModule - Add safety check to prevent multiple Koin initialization Co-Authored-By: Claude Sonnet 4.5 --- IMPLEMENTATION_STATUS.md | 2 +- README.md | 2 +- composeApp/proguard-rules.pro | 9 ++++++--- composeApp/src/commonMain/kotlin/di/DatabaseModule.kt | 1 - composeApp/src/commonMain/kotlin/di/PlatformSDK.kt | 7 +++++++ 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/IMPLEMENTATION_STATUS.md b/IMPLEMENTATION_STATUS.md index 170ae36..407d073 100644 --- a/IMPLEMENTATION_STATUS.md +++ b/IMPLEMENTATION_STATUS.md @@ -81,7 +81,7 @@ - Project deletion is handled safely (nullifies habit references before deletion) - All filtering is optional (null projectId shows all habits) - Color parsing is cross-platform compatible (no android.graphics.Color dependency) -- Follows existing codebase patterns: BaseViewModel, sealed events/actions, DI via Kodein +- Follows existing codebase patterns: BaseViewModel, sealed events/actions, DI via Koin ## Files Created (16) - core/database/migrations/Migration7to8.kt diff --git a/README.md b/README.md index 2efc57a..f03763f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Adopted to full Compose Multiplatform and Kotlin Multiplatform - Presentation: KViewModel - Database: Room - Resources: LibRes -- DI: Kodein +- DI: Koin - UI: Compose Multiplatform ### Supported Platforms diff --git a/composeApp/proguard-rules.pro b/composeApp/proguard-rules.pro index 87b18b5..99ed579 100644 --- a/composeApp/proguard-rules.pro +++ b/composeApp/proguard-rules.pro @@ -22,9 +22,12 @@ -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} -# Keep Kodein --keep class org.kodein.** { *; } --keep @org.kodein.di.DI$Tag class * +# Keep Koin +-keep class org.koin.** { *; } +-keep class org.koin.core.** { *; } +-keepclassmembers class * { + public (...); +} # General Android rules -keepclassmembers class * implements android.os.Parcelable { diff --git a/composeApp/src/commonMain/kotlin/di/DatabaseModule.kt b/composeApp/src/commonMain/kotlin/di/DatabaseModule.kt index 7bef409..df372c0 100644 --- a/composeApp/src/commonMain/kotlin/di/DatabaseModule.kt +++ b/composeApp/src/commonMain/kotlin/di/DatabaseModule.kt @@ -6,7 +6,6 @@ import feature.daily.data.DailyDao import feature.habits.data.HabitDao import feature.projects.data.ProjectDao import feature.tracker.data.TrackerDao -import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val databaseModule = module { diff --git a/composeApp/src/commonMain/kotlin/di/PlatformSDK.kt b/composeApp/src/commonMain/kotlin/di/PlatformSDK.kt index 395b1b9..1684183 100644 --- a/composeApp/src/commonMain/kotlin/di/PlatformSDK.kt +++ b/composeApp/src/commonMain/kotlin/di/PlatformSDK.kt @@ -1,7 +1,9 @@ package di import org.koin.core.Koin +import org.koin.core.context.GlobalContext import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin import org.koin.core.qualifier.named import org.koin.dsl.module @@ -14,6 +16,11 @@ object PlatformSDK { configuration: PlatformConfiguration, appDatabase: Any? = null ) { + // Stop any existing Koin instance to allow reinitialization + if (GlobalContext.getOrNull() != null) { + stopKoin() + } + val configModule = module { single { configuration } if (appDatabase != null) {