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

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 !

I think it related with packageName in your Gradle:
packageName = "com.example.kmp_app.db"
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"
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
}

Related

Extension of type 'LibraryExtension' does not exist

I created a gradle plugin to unify some settings between the various modules of the application. the summary of the error is this:
org.gradle.api.plugins.InvalidPluginException: An exception occurred applying plugin request [id: 'common.plugin']
Caused by: org.gradle.api.UnknownDomainObjectException: Extension of type 'LibraryExtension' does not exist. Currently registered extension types: [ExtraPropertiesExtension,....]
This is a summary image of the project architecture:
CommonPluginClass:
class CommonPluginClass : Plugin<Project> {
private val consumerProguardFileName = "consumer-rules.pro"
private val proguardFileName = "proguard-rules.pro"
private val sdkToCompile = 33
override fun apply(project: Project) {
println(">>> Adding sugar to gradle files!")
with(project)
{
applyPlugins(this)
androidConfig(this)
}
println(">>> Sugar added for core Module!")
}
private fun applyPlugins(project: Project) {
println(">>> apply plugins!")
project.pluginManager.run {
apply("com.android.application")
apply("kotlin-android")
apply("kotlin-kapt")
}
println(">>> end apply plugins!")
}
private fun androidConfig(project: Project) {
project.extensions.configure<LibraryExtension>{
defaultConfig.targetSdk = 33
}
}
}
the error occurs inside the androidConfig function when calling configure
I was inspired by now android, dependencies and imports are similar but not build. Can someone unlock it please.
build-logic:convention build.gradle
plugins {
alias(libs.plugins.kotlin.jvm) apply false
id "org.gradle.kotlin.kotlin-dsl" version "2.4.1"
}
dependencies {
compileOnly(libs.android.pluginGradle)
compileOnly(libs.kotlin.pluginGradle)
}
gradlePlugin {
plugins {
commonPlugin {
id = "common.plugin"
implementationClass = "CommonPluginClass"
}
}
}
LITTLE UPDATE:
I've noticed that any module I enter always identifies it to me as ApplicationExtension
I found the solution and as usual it's a stupid thing.
the application extension use plugin
apply("com.android.application")
the library or module use plugin
apply("com.android.library")

Kotlin Multiplatform: Can't reference class in commonMain sourceset from another module's androidMain sourceset

I have two multiplatform modules shared and other in a standard multiplatform template project that targets Android and iOS.
shared defines a class in commonMain source set
class SharedGreeting()
other is setup to depend on shared like this in the gradle file:
val commonMain by getting {
dependencies {
implementation(project(":shared"))
}
}
And in its androidMain sourceset it tries to reference SharedGreeting in some class, fx:
class AndroidGreeter{
val foo = SharedGreeting()
}
But no matter what I try, I get IDE errors when I try to reference the shared class, and I have to manually add an import statement.
The code compiles and deploys without problems though!
Any ideas on what I am missing or misunderstanding? Or is this a bug in KMM?
Full copy of other gradle file:
plugins {
kotlin("multiplatform")
kotlin("native.cocoapods")
id("com.android.library")
}
version = "1.0"
kotlin {
android()
iosX64()
iosArm64()
iosSimulatorArm64()
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
ios.deploymentTarget = "14.1"
framework {
baseName = "other"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":shared"))
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val androidMain by getting
val androidTest by getting
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
val iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}
}
}
android {
compileSdk = 32
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 24
targetSdk = 32
}
}
For full project source code:
https://github.com/RabieJradi/kmm_import_error_sample
IDE suggested action for adding a dependency doesn't unfortunately do anything.
Turns out it is a bug in KMM and a fix have been implemented by the Jetbrains team here https://youtrack.jetbrains.com/issue/KTIJ-22056/KMM-Cant-reference-shared-module-commonMain-source-set-classes-from-another-modules-androidMain-source-set
You cannot do this in that way.
To make your project work you have to work with expect and actual words.
So, make these changes:
change your SharedGreeting class in shared module like that:
expect class SharedGreeting {
fun greeting(): String
}
Then your IDE wants you to make two changes, in shared module.
Add a class in both modules, iosMain and androidMain named SharedGreeting. The code is the same for both classes:
actual class SharedGreeting {
actual fun greeting(): String {
return "Hello, ${Platform().platform}!"
}
}
The work is done, now your other library has no errors. From your androidMain module inside "other" you can work only on other androidMain modules inside other MKK libraries.

Android - Gradle - Unresolved reference: VersionCatalogsExtension

I'm trying to utilize Gradle versionCatalogs and put all the versions there including Android-specific compileSdkVersion, minSdkVersion, targetSdkVersion.
It's possible to access these versions with the following code:
val libs = rootProject.extensions.getByType(VersionCatalogsExtension::class.java).named("libs")
libs.findVersion("compileSdkVersion").get().toString() as Integer
But my project is multi-module and I don't want to copy-paste boilerplate code. So, I created extension functions in a separate module.
root-level settings.gradle.kts:
includeBuild("gradle-dependencies")
module-level build.gradle.kts:
plugins {
`kotlin-dsl`
`java-gradle-plugin`
}
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
group = "com.foobar.androidtools.gradle"
version = "SNAPSHOT"
dependencies {
implementation(gradleApi())
}
gradlePlugin {
plugins.register("Gradle extensions") {
id = "foobar-gradle-dependencies"
implementationClass = "com.foobar.gradle_dependencies.DependenciesPlugin"
}
}
src/main/kotlin/com/foobar/gradle_dependencies/Extensions.kt:
package com.foobar.gradle_dependencies
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
fun Project.version(key: String): String = this.extensions
.getByType<VersionCatalogsExtension>()
.named("libs")
.findVersion(key)
.get()
.requiredVersion
fun Project.versionInt(key: String) = version(key).toInt()
val Project.KOTLIN_JVM_TARGET_VERSION get() = version("kotlinJvmTargetVersion")
val Project.ANDROID_COMPILE_SDK_VERSION get() = versionInt("compileSdkVersion")
val Project.ANDROID_MIN_SDK_VERSION get() = versionInt("minSdkVersion")
val Project.ANDROID_TARGET_SDK_VERSION get() = versionInt("targetSdkVersion")
Trying to build the project I see the following error:
e: /AndroidTools/gradle-dependencies/src/main/kotlin/com/foobar/gradle_dependencies/Extensions.kt: (4, 33): Unresolved reference: VersionCatalogsExtension
Why the dependency can't be resolved taking into account that import works fine?
AGP: 7.1.3
Gradle: https://services.gradle.org/distributions/gradle-7.4.2-bin.zip

gradle kotlin dsl: how to create a shared function which uses a plugin class?

A simplified child module build.gradle.kts:
plugins {
id("com.android.library")
kotlin("android")
}
android {
androidComponents.beforeVariants { it: com.android.build.api.variant.LibraryVariantBuilder ->
it.enabled = run {
// logic to calculate if
it.productFlavors[0].second == "flavor" && it.buildType == "debug"
}
}
}
Is it possible to extract function for calculation of enabled state of buildVariant?
fun calculateIsEnabled(lvb: com.android.build.api.variant.LibraryVariantBuilder): Boolean {
return lvb.productFlavors[0].second == "flavor" && lvb.buildType == "debug"
}
I tried to declare the function in the root build.gradle.kts but I don't know how to access it from submodule and if it is possible at all
I tried to declare it in buildSrc module, but com.android.build.api.variant.LibraryVariantBuilder is undefined here because the plugin com.android.library is not present here and I think it is not allowed and/or meaningless
So, the question is: where to declare a shared function that uses types defined in a gradle plugin and need to be accessible in all submodules of type android library?
After several tries I solved it:
buildSrc/build.gradle.kts
repositories {
google()
mavenCentral()
}
plugins {
`kotlin-dsl`
}
dependencies {
// important: dependency only in simple string format!
implementation("com.android.tools.build:gradle:7.2.0-alpha03")
}
buildSrc/src/main/kotlin/Flavors.kt
import com.android.build.api.variant.LibraryVariantBuilder
import com.android.build.api.variant.ApplicationVariantBuilder
private fun isFlavorEnabled(flavor1: String, buildType: String): Boolean {
return flavor1 == "flavor" && buildType == "debug"
}
fun isFlavorEnabled(lvb: LibraryVariantBuilder): Boolean {
// productFlavors are pairs of flavorType(dimension) - flavorName(selectedFlavor)
return lvb.run { isFlavorEnabled(productFlavors[0].second, buildType ?: "") }
}
fun isFlavorEnabled(avb: ApplicationVariantBuilder): Boolean {
return avb.run { isFlavorEnabled(productFlavors[0].second, buildType ?: "") }
}
In library/build.gradle.kts and app/build.gradle.kts
android {
androidComponents.beforeVariants {
it.enabled = isFlavorEnabled(it)
}
}

Using Android library in Kotlin multiplatform library

I have three targets commonMain/androidMain/iOSMain respectively. Because I need to access the assets in Android devices in androidMain module. I found I cannot use the Android API... The following is part of my build.gradle.kts:
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
id("com.android.application")
kotlin("multiplatform")
}
repositories {
google()
jcenter()
}
android {
compileSdkVersion(29)
buildToolsVersion("29.0.1")
defaultConfig {
minSdkVersion(19)
targetSdkVersion(29)
}
sourceSets {
getByName("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
java.srcDirs(file("src/androidMain/kotlin"))
res.srcDirs(file("src/androidMain/res"))
}
}
}
kotlin {
//select iOS target platform depending on the Xcode environment variables
val iOSTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
::iosArm64
else
::iosX64
iOSTarget("ios") {
binaries {
framework {
baseName = "Example"
}
}
}
jvm("android")
sourceSets["commonMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-common")
}
sourceSets["androidMain"].dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
}
sourceSets["commonTest"].dependencies {
implementation ("org.jetbrains.kotlin:kotlin-test")
implementation ("org.jetbrains.kotlin:kotlin-test-junit")
}
}
How can I use Android library in androidMain? For example,
val inputStream = assets.open("Test.txt")
Make sure to create an AndroidManifest.xml with (library module) package name different than the (app module) package name underneath the SharedCode/src directory. And as Nagy Robi said use android() instead of jvm('android'). Please check the below references also:
Adding an Android Target to a Kotlin Multiplatform Project
Multiplatform sample
Try using android() instead of jvm('android') to load the target from the presets.

Categories

Resources