Search code examples
android-roomkotlin-multiplatform

Implement Room db in a KMM project with WasmJs support


I am building a KMP project that supports Android, iOS, desktop(jvm) and Web(wasmJs)... Within, I am implementing Room for database storage.

Since WasmJS doesn't support Room, I created a new source set for Android, iOS and Desktop and called it nonJS

Here is my gradle:

kotlin {
    @OptIn(ExperimentalWasmDsl::class)
    wasmJs {
        moduleName = "data"
        browser {
            commonWebpackConfig {
                outputFileName = "composeApp.js"
                devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
                    static = (static ?: mutableListOf()).apply {
                        // Serve sources to debug inside browser
                        add(project.projectDir.path)
                    }
                }
            }
        }
        binaries.executable()
    }

    androidTarget {
        @OptIn(ExperimentalKotlinGradlePluginApi::class)
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_17)
        }
    }

    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "data"
            isStatic = true
        }
    }
    jvm("desktop")

    @OptIn(ExperimentalKotlinGradlePluginApi::class)
    applyDefaultHierarchyTemplate {
        // create a new group that
        // depends on `common`
        common {
            // Define group name without
            // `Main` as suffix
            group("nonJs") {
                // Provide which targets would
                // be part of this group
                withAndroidTarget()
                withIos()
                withJvm()
            }
        }
    }

    sourceSets {
        all{
            languageSettings {
                @OptIn(ExperimentalKotlinGradlePluginApi::class)
                compilerOptions{
                    freeCompilerArgs.add("-Xexpect-actual-classes")
                }
            }
        }

        commonMain.dependencies {
            api(project(":domain"))

            implementation(libs.ktor.client.logging)
            implementation(libs.ktor.client.content.negotiation)
        }

        val nonJsMain by getting {
            dependencies {
                api(libs.room.runtime)
                implementation(libs.sqlite.bundled)
            }
        }

        androidMain.dependencies {
            implementation(libs.ktor.client.android)
            implementation(libs.ktor.client.okhttp)

            implementation(libs.paging.room)
        }

        iosMain.dependencies {
            implementation(libs.ktor.client.darwin)
        }

        val desktopMain by getting {
            dependencies {
                implementation(libs.ktor.client.okhttp)
            }
        }

        wasmJsMain.dependencies {
            implementation(libs.ktor.client.js)
        }
    }

    task("testClasses")
}

Then I Created an expected Database Builder for these platforms in the nonJS folder.

DatabaseBuilder.kt

expect class DatabaseBuilder() {
    fun databaseBuilder(): RoomDatabase.Builder<AppDatabase>
}

Then I started implementing the actual files

example DatabaseBuilder.jvm.kt

actual class DatabaseBuilder {
    actual fun databaseBuilder(): RoomDatabase.Builder<AppDatabase> {
        val dbFile = File(System.getProperty("java.io.tmpdir"), "movies_database.db")
        return Room.databaseBuilder<AppDatabase>(
            name = dbFile.absolutePath,
        )
    }
}

Also DatabaseBuilder.iOS.kt

actual class DatabaseBuilder {
    actual fun databaseBuilder(): RoomDatabase.Builder<AppDatabase> {
        val dbFilePath = NSHomeDirectory() + "/movies_database.db"
        return Room.databaseBuilder<AppDatabase>(
            name = dbFilePath,
            factory = { AppDatabase::class.instantiateImpl() }
        )
    }
}

Now the problem is:

  • Android and Desktop implementations build fine with no problem.
  • However, iOS implementation doesn't recognize RoomDatabase dependencies. Infact it doesn't recognize any of the nonJS dependencies or files as AppDatabase.
  • There is nothing difference between Android, Desktop and iOS.

I think there is something wrong with my nonJS declaration block

@OptIn(ExperimentalKotlinGradlePluginApi::class)
applyDefaultHierarchyTemplate {
    // create a new group that
    // depends on `common`
    common {
        // Define group name without
        // `Main` as suffix
        group("nonJs") {
            // Provide which targets would
            // be part of this group
            withAndroidTarget()
            withIos()
            withJvm()
        }
    }
}

Any help with that?? OR Is there a better approach to my project??


Solution

  • I found out what was wrong... As expected the issue was in my Hierarchy

    Wrong Hierarchy:

    @OptIn(ExperimentalKotlinGradlePluginApi::class)
    applyDefaultHierarchyTemplate {
        common {
            group("nonJs") {
                withAndroidTarget()
                withIos()
                withJvm()
            }
        }
    }
    

    Which lead to this graph: Wrong Hierarchy

    As you can see iOSMain target doesn't exist... Instead, iOS is separated into its main components IosArm64 and IosX64... That is why my iOS classes in my iOSMain folder couldn't resolve any dependencies.


    Correct Hierarchy:

    @OptIn(ExperimentalKotlinGradlePluginApi::class)
    applyDefaultHierarchyTemplate {
        common {
            group("nonJs") {
                withAndroidTarget()
                withJvm()
                group("ios") {
                    withIos()
                }
            }
        }
    }
    

    Which lead to this graph: Correct Hierarchy

    As you can see... Here, Ios is created a stand alone group within nonJs... IosArm64 and IosX64 are added within the "iOSMain" after that. This way my iOS classes in my iOSMain folder could resolve dependencies.