androidandroid-roomkotlin-multiplatformkoincompose-multiplatform

Koin NoDefinitionFoundException for Room Database in a Compose Multiplatform Project


I'm working on a Compose Multiplatform project using Koin for dependency injection, Voyager for Navigation and Room for Database Management. I'm encountering a NoDefinitionFoundException when trying to provide the ForgoDatabase instance using a RoomDatabase.Builder.

It's my first time working in a Compose Multiplatform. My main source for all libraries is their documentation and some implementations on Github for guidance. I've checked many implementations in different approaches and I can't figure out what my mistake is.

Warning from the Koin Plugin: enter image description here

My build.gradle:

import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidApplication)
    alias(libs.plugins.composeMultiplatform)
    alias(libs.plugins.composeCompiler)
    alias(libs.plugins.ksp.plugin)
    alias(libs.plugins.room.gradle.plugin)
}

kotlin {
    androidTarget {
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_11)
        }
    }
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "ComposeApp"
            isStatic = true
            linkerOpts.add("-lsqlite3")
        }
    }

    room {
        schemaDirectory("$projectDir/schemas")
    }
    
    sourceSets {

        commonTest.dependencies {
            implementation(libs.kotlin.test)
        }
        
        androidMain.dependencies {
            implementation(compose.preview)
            implementation(libs.androidx.activity.compose)
            implementation(libs.koin.android)
            implementation(libs.koin.androidx.compose)
        }
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
            implementation(compose.components.resources)
            implementation(compose.components.uiToolingPreview)
            implementation(libs.androidx.lifecycle.viewmodel)
            implementation(libs.androidx.lifecycle.runtime.compose)
            implementation(libs.kotlinx.date.time)
            implementation(libs.kotlinx.coroutines)
            // Voyager
            implementation(libs.bundles.voyager)
            // Koin
            implementation(libs.koin.compose)
            implementation(libs.koin.viewmodel)
            api(libs.koin.core)
            // Room
            implementation(libs.room.runtime)
            implementation(libs.sqlite)
        }

        all {
            languageSettings.optIn("kotlin.ExperimentalMultiplatform")
            languageSettings.optIn("kotlin.RequiresOptIn")
        }
    }
    compilerOptions {
        freeCompilerArgs.add("-Xexpect-actual-classes")
    }
}

android {
    namespace = "com.feeltheboard.forgo"
    compileSdk = libs.versions.android.compileSdk.get().toInt()

    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    sourceSets["main"].res.srcDirs("src/androidMain/res")
    sourceSets["main"].resources.srcDirs("src/commonMain/composeResources")

    defaultConfig {
        applicationId = "com.feeltheboard.forgo"
        minSdk = libs.versions.android.minSdk.get().toInt()
        targetSdk = libs.versions.android.targetSdk.get().toInt()
        versionCode = 2
        versionName = "1.0.2"
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
}

dependencies {
    debugImplementation(compose.uiTooling)
    add("kspCommonMainMetadata", libs.room.ksp)
    add("kspAndroid", libs.room.ksp)
    add("kspIosSimulatorArm64", libs.room.ksp)
    add("kspIosX64", libs.room.ksp)
    add("kspIosArm64", libs.room.ksp)
}

Libraries Versions:

[versions]
agp = "8.9.0"
android-compileSdk = "35"
android-minSdk = "24"
android-targetSdk = "35"
androidx-activityCompose = "1.10.1"
androidx-appcompat = "1.7.0"
androidx-constraintlayout = "2.2.1"
androidx-core-ktx = "1.15.0"
androidx-espresso-core = "3.6.1"
androidx-lifecycle = "2.8.4"
androidx-material = "1.12.0"
androidx-test-junit = "1.2.1"
compose-multiplatform = "1.7.0"
kotlinx-coroutines="1.8.1"
kotlinx-date-time="0.6.2"
junit = "4.13.2"
koin = "4.0.2"
kotlin = "2.1.0"
ksp = "2.1.0-1.0.29"
room = "2.7.0-rc02"
sqlite = "2.5.0-alpha01"
voyager = "1.1.0-beta02"

Here's my SharedModule in CommonMain:

expect val platformModule: Module

val sharedModule = module {

    single { getRoomDatabase(get()) }
    single<ForgoRepository> { ForgoRepositoryImpl(get()) }
    factory { HomeViewModel(get()) }
    factory { TaskViewModel(get()) }
}

fun appModules() = listOf(sharedModule, platformModule)

The Room database implementation + database builder on CommonMain:

@Database(entities = [Task::class], version = 5, exportSchema = true)
@ConstructedBy(ForgoDatabaseConstructor::class)
abstract class ForgoDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object ForgoDatabaseConstructor : RoomDatabaseConstructor<ForgoDatabase> {
    override fun initialize(): ForgoDatabase
}

fun getRoomDatabase(
    builder: RoomDatabase.Builder<ForgoDatabase>
): ForgoDatabase {
    return builder
        .fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true)
        .setDriver(BundledSQLiteDriver())
        .setQueryCoroutineContext(Dispatchers.IO)
        .build()
}

The Android Main implementation with builder, modules, mainActivity and application (added to manifest):

fun getDatabaseBuilder(
    ctx: Context
): RoomDatabase.Builder<ForgoDatabase> {

    val appContext = ctx.applicationContext
    val dbFile = appContext.getDatabasePath("task.db")

    return Room.databaseBuilder<ForgoDatabase>(
        context = appContext,
        name = dbFile.absolutePath
    )
}

actual val platformModule: Module = module {
    single { getDatabaseBuilder(ctx = get()) }
}

class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        initializeKoin {
            androidContext(this@MainApplication)
        }
    }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            App()
        }
    }
}


Solution

  • Ultimately, the main problem was not making my DAO instance available.

    Here's my updated KoinModules.kt

    expect val platformModule: Module
    
    val sharedModule = module {
    
        single { getRoomDatabase(get()) }
    
        single { get<ForgoDatabase>().taskDao() }
        single { get<ForgoDatabase>().tagDao() }
    
        single<ForgoRepository> { ForgoRepositoryImpl(get(), get()) }
        factory { HomeViewModel(get()) }
        factory { TaskViewModel(get()) }
    }
    
    fun appModules() = listOf(sharedModule, platformModule)
    

    After creating the database instance, following the dependency chain, I could create the Dao instance and use it in repositories. Making available for Viewmodels.