androidkotlinkmm

Actual/Expect classes doesn't work in Kotlin Mutliplatform proyect


I was develoing an KMM App, where I try to implement a regular localDatasource and remoteDatasource, using SQLDelight and Ktor respectively.

My problems comes when I try to shared the native code from AndroidApp and iosMain, into commonModule. I start to get the following error into my commonModule expect class:

Expected function 'cache' has no actual declaration in module KMM-APP.shared for JVM
Expected function 'cache' has no actual declaration in module KMM-APP.shared.iosArm64Main for Native
Expected function 'cache' has no actual declaration in module KMM-APP.shared.iosX64Main for Native

It's a bit confuse, in order I don't make use of jvm module in my proyect, although I do for IOS module.

Here it's my cacheAndroid.kt of AndroidApp module:

import android.content.Context
import com.example.kmp_app.db.PetsDatabase
import com.squareup.sqldelight.android.AndroidSqliteDriver

lateinit var appContext: Context

internal actual fun cache(): PetsDatabase {
  val driver = AndroidSqliteDriver(PetsDatabase.Schema, appContext, "petsDB.db")
  return PetsDatabase(driver)
}

Here is the classes of my IOS module:

import com.example.kmp_app.db.PetsDatabase
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver

internal actual fun cache(): PetsDatabase {
    val driver = NativeSqliteDriver(PetsDatabase.Schema, "petsDB.db")
    return PetsDatabase(driver)
}

And the use into commonModule:

internal expect fun cache(): PetsDatabase

I in this last line of code where I reciving the error above, but I also get the error into the actual classes of Android and IOS modules, into their expect class variant.

Finally regarding my build.gradle(common)

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")
    id("kotlinx-serialization")
    id("com.squareup.sqldelight")
}

version = "1.0"

kotlin {
    targets{
        ios {
            binaries {
                framework {
                    baseName = "shared"
                }
            }
        }
        // Block from https://github.com/cashapp/sqldelight/issues/2044#issuecomment-721299517.
        val onPhone = System.getenv("SDK_NAME")?.startsWith("iphoneos") ?: false
        if (onPhone) {
            iosArm64("ios")
        } else {
            iosX64("ios")
        }

        android()
        //iosSimulatorArm64() sure all ios dependencies support this target
    }

    cocoapods {
        summary = "Some description for the Shared Module"
        homepage = "Link to the Shared Module homepage"
        ios.deploymentTarget = "14.1"
        podfile = project.file("../iosApp/Podfile")
    }
    
    sourceSets {
        all {
            languageSettings.apply {
                useExperimentalAnnotation("kotlinx.coroutines.ExperimentalCoroutinesApi")
            }
        }

        val commonMain by getting{
            dependencies {
                implementation(kotlin("stdlib-common"))
                implementation(Coroutines.Core.core)
                implementation(Ktor.Core.common)
                implementation(Ktor.Json.common)
                implementation(Ktor.Logging.common)
                implementation(Ktor.Serialization.common)
                implementation(SqlDelight.runtime)

            }
        }
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test-common"))
                implementation(kotlin("test-annotations-common"))
                implementation(Ktor.Mock.common)
            }
        }
        val androidMain by getting{
            dependencies {
                implementation(kotlin("stdlib"))
                implementation(Coroutines.Core.core)
                implementation(Ktor.android)
                implementation(Ktor.Core.jvm)
                implementation(Ktor.Json.jvm)
                implementation(Ktor.Logging.jvm)
                implementation(Ktor.Logging.slf4j)
                implementation(Ktor.Mock.jvm)
                implementation(Ktor.Serialization.jvm)
                implementation(Serialization.core)
                implementation(SqlDelight.android)
            }
        }

        val androidAndroidTestRelease by getting
        val androidTest by getting {
            dependsOn(androidAndroidTestRelease)
            dependencies {
                implementation(kotlin("test-junit"))
                implementation("junit:junit:4.13.2")
            }
        }
        val iosX64Main by getting
        val iosArm64Main by getting
        //val iosSimulatorArm64Main by getting
        val ios by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            //iosSimulatorArm64Main.dependsOn(this)
            dependencies {
                implementation(SqlDelight.native)
            }
        }
    }
}

android {
    compileSdk = 31
    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    defaultConfig {
        minSdk = 21
        targetSdk = 31
        versionCode = 1
        versionName = "1.0"
    }
}
sqldelight {
    database("PetsDatabase") {
        packageName = "com.example.kmp_app.db"
        sourceFolders = listOf("sqldelight")
    }
}

And my proyect build.gradle:

buildscript {
    repositories {
        google()
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:4.2.0")
        classpath(kotlin("gradle-plugin", version = Versions.kotlin))
        classpath(kotlin("serialization", version = Versions.kotlin))
        classpath("com.squareup.sqldelight:gradle-plugin:${Versions.sqldelight}")
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
        jcenter()
    }
}

plugins{
    //kotlin("android") version "${Versions.kotlin}" apply false
}

I hope you can help and if like this, take thanks in advance !


Solution

  • I think it related with packageName in your Gradle:

     packageName = "com.example.kmp_app.db"
    
    1. Try to pass route of your cache function instead of "com.example.kmp_app.db" like if my cache function exists on dataSource.cacheSource, we will pass "com.example.kmp_app.db.dataSource.cacheSource"

    2. Be sure your Cache actual / expect function have the same package name like this "com.example.kmp_app.db.dataSource.cacheSource"

    Shared gradle

    sqldelight {
        database("RecipeDatabase") {
            packageName = "com.example.food1fork.Food1ForkKmm.DataSource.cacheSource"
            sourceFolders = listOf("SqlDelight")
        }
    }
    

    iOS module

    package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
    
    actual class DriverFactory {
        actual fun createDriver(): SqlDriver {
            return NativeSqliteDriver(RecipeDatabase.Schema, "recipes.db")
        }
    }
    

    Android module

    package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
    
    actual class DriverFactory(private val context: Context) {
        actual fun createDriver(): SqlDriver {
            return AndroidSqliteDriver(RecipeDatabase.Schema, context, "recipes.db")
        }
    }
    

    Shared module

    package com.example.food1fork.Food1ForkKmm.DataSource.cacheSource
    
    expect class DriverFactory {
        fun createDriver(): SqlDriver
    }