4

I was developing an Kotlin Multiplatform App where I would like to use @Parcelize annotation in my model class. But in Kotlin Multiplatform plugins the @Parcelize annotation, in the kotlin version which I'm using is in the android.extensions plugin, which apply to androidApp module.

regarding my build.gradle.kts(androidApp)

plugins {
  id("com.android.application")
  kotlin("android")
  kotlin("android.extensions")
  kotlin("kapt")
  id("kotlinx-serialization")
  id("androidx.navigation.safeargs.kotlin")
}

android {
  compileSdkVersion(Versions.compileSdk)

  compileOptions{
    sourceCompatibility = org.gradle.api.JavaVersion.VERSION_1_8
    targetCompatibility = org.gradle.api.JavaVersion.VERSION_1_8
  }

  kotlinOptions{
    jvmTarget = JavaVersion.VERSION_1_8.toString()
  }

  kapt{
    generateStubs = true
    correctErrorTypes = true
  }

  androidExtensions{
    isExperimental = true
  }

  buildFeatures{
    dataBinding = true
    viewBinding = true
  }

  defaultConfig {
    applicationId = "com.jshvarts.kmp.android"
    minSdkVersion(Versions.minSdk)
    targetSdkVersion(Versions.targetSdk)
    versionCode = 1
    versionName = "1.0"

    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
  }

  buildTypes {
    getByName("release") {
      isMinifyEnabled = false
      proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
    }
  }

  packagingOptions {
    exclude("META-INF/*.kotlin_module")
  }
}

dependencies {
  implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
  implementation(kotlin("stdlib-jdk8", Versions.kotlin))
  implementation(Coroutines.android)
  implementation(AndroidX.appCompat)
  implementation(AndroidX.constraintLayout)
  implementation(AndroidX.recyclerView)
  implementation(AndroidX.lifecycleExtensions)
  implementation(AndroidX.lifecycleViewModelKtx)
  implementation(material)
  implementation(AndroidX.swipeToRefreshLayout)
  implementation(timber)
  implementation(picasso)
  implementation(AndroidX.navigation)
  implementation(AndroidX.navigation_ui)
  implementation(Serialization.runtime)
  //implementation(Serialization.core)
  //Dependency for googlePay
  implementation("com.google.android.gms:play-services-wallet:16.0.1")
  kapt(databinding)
  implementation(glide){
    exclude( "com.android.support")
  }
  kapt(glide)
  implementation(project(":shared"))

}

build.gradle.kts(shared)

plugins {
  id("com.android.library")
  kotlin("multiplatform")
  kotlin("plugin.serialization")
  //id("kotlinx-serialization")
  id("org.jetbrains.kotlin.native.cocoapods")
  id("com.squareup.sqldelight")
}
// CocoaPods requires the podspec to have a version.
version = "1.0"

android {
  compileSdkVersion(Versions.compileSdk)
  buildToolsVersion(Versions.androidBuildTools)

  defaultConfig {
    minSdkVersion(Versions.minSdk)
    targetSdkVersion(Versions.targetSdk)
    versionCode = 1
    versionName = "1.0"
  }
}

version = "1.0"
dependencies {
  implementation("com.google.firebase:firebase-crashlytics-buildtools:2.8.1")
  implementation(project(mapOf("path" to ":androidApp")))
}

kotlin {
  targets {

    val sdkName: String? = System.getenv("SDK_NAME")

    val isiOSDevice = sdkName.orEmpty().startsWith("iphoneos")
    if (isiOSDevice) {
      iosArm64("iOS")
    } else {
      iosX64("iOS")
    }
    android()
  }

  cocoapods {
    // Configure fields required by CocoaPods.
    summary = "Description for a Kotlin/Native module"
    homepage = "Link to a Kotlin/Native module homepage"
  }

  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)
        implementation(Serialization.runtime)
        //implementation(project(":androidApp"))
        //implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}")
        //implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}")
        //implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
      }
    }

    val commonTest by getting {
      dependencies {
        implementation(Ktor.Mock.jvm)
      }
    }

    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.runtime)
        //implementation(Serialization.core)
        implementation(SqlDelight.android)


      }
    }

    val androidTest by getting {
      dependencies {
        implementation(kotlin("test-junit"))
        implementation(Ktor.Mock.common)
      }
    }

    val iOSMain by getting {
      dependencies {
        implementation(Coroutines.Core.core)
        implementation(Ktor.ios)
        implementation(Ktor.Core.common)
        implementation(Ktor.Json.common)
        implementation(Ktor.Logging.common)
        implementation(Ktor.Serialization.jvm)
       // implementation(Serialization.runtimeNative)
        implementation(SqlDelight.runtime)
        implementation(Ktor.Mock.common)
      }
    }

    val iOSTest by getting {
      dependencies {
        implementation(Ktor.Mock.native)
      }
    }
  }
}


sqldelight {
  database("PetsDatabase") {
    packageName = "com.jshvarts.kmp.db"
    sourceFolders = listOf("sqldelight")
  }
}

And my project build.gradle.kts

// Top-level build file where you can add configuration options common to all sub-projects/modules.

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

    dependencies {
        classpath("com.android.tools.build:gradle:4.0.0")
        classpath(kotlin("gradle-plugin", version = Versions.kotlin))
        classpath(kotlin("serialization", version = Versions.kotlin))
        classpath("com.squareup.sqldelight:gradle-plugin:${Versions.sqldelight}")
        classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0")
        classpath ("androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.navigation}")
        classpath ("org.jetbrains.kotlin:kotlin-android-extensions-runtime:${Versions.kotlin}")
    }
}

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

//TODO("Probar bajando a kotlin version 1.3.72, y habilitando el android-extensions")
plugins {
  //kotlin("jvm") version "${Versions.kotlin}"
  id("org.jlleitschuh.gradle.ktlint") version "9.2.1"
  id ("com.github.ben-manes.versions") version "0.28.0"
  //kotlin("android") version "${Versions.kotlin}" apply false
  //id("org.jetbrains.kotlin.plugin.parcelize") version "${Versions.kotlin}"
}
apply(from = "quality/lint.gradle") 

So I create a expect and actual Parcelable and Parcelize class in androidApp and shared modules:

androidApp

actual typealias Parcelable = android.os.Parcelable

actual typealias Parcelize = kotlinx.android.parcel.Parcelize

and in shared module

// Common Code

expect interface Parcelable

@UseExperimental(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class Parcelize()

So in those class I receiving the following error:

In Parcelable(shared)

Expected interface 'Parcelable' has no actual declaration in module KmpMVVMGooglePay.shared for JVM
Expected interface 'Parcelable' has no actual declaration in module KmpMVVMGooglePay.shared.iOSMain for Native

and in android classes:

Actual typealias 'Parcelable' has no corresponding expected declaration

Actual typealias 'Parcelize' has no corresponding expected declaration

So what I missing about actual/expect keywords behavior?

Thanks for the help in advance!

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Manuel Lucas
  • 636
  • 6
  • 17

2 Answers2

3

These snippets show you how to use Android Parcelable in a KMM project for any type of class including primitives. It shows proper us of Annotations, Interfaces, Generics, Objects, @TypeParceler, Parceler, Parcelable, Parcelize, and how to implement each platform for common code, iOS and Android.

This is code is necessary to prevent crashes when the Android app is put into the background and the Parceler is automatically run to save state.

iOS does not use Parcel, so we need to stub it out on the iOS side.

In this example, I'm using the non-natively-parcelable class of LocalDateTime as the example non-native-parcelable class. You can use any class, just change the implementation.

in build.gradle.kts(:shared)

plugins {
    kotlin("multiplatform")
    id("com.android.library")
    id("kotlin-parcelize") // add this
    id("kotlin-kapt") // add this
    // ...rest of defintions...
}
kotlin {
    android()
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
        }
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")  // LocalDateTime library written in Kotlin (can't use java libraries)
            }
        }
        // ...rest of definitions...
     }
  // ...rest of definitions...
}

in commonMain/.../Platform.kt

import kotlinx.datetime.LocalDateTime

// For Android @Parcelize
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class CommonParcelize()

// For Android Parcelable
expect interface CommonParcelable

// For Android @TypeParceler
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Retention(AnnotationRetention.SOURCE)
@Repeatable
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
expect annotation class CommonTypeParceler<T, P : CommonParceler<in T>>()

// For Android Parceler
expect interface CommonParceler<T>

// For Android @TypeParceler to convert LocalDateTime to Parcel
expect object LocalDateTimeParceler: CommonParceler<LocalDateTime>

in androidMain/.../Platform.kt

IMPORTANT NOTE: must import kotlinx.parcelize.* NOT kotlinx.android.parcel.*

import android.os.Parcel
import android.os.Parcelable
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toLocalDateTime
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler

actual typealias CommonParcelize = Parcelize
actual typealias CommonParcelable = Parcelable

actual typealias CommonParceler<T> = Parceler<T>
actual typealias CommonTypeParceler<T,P> = TypeParceler<T, P>
actual object LocalDateTimeParceler : Parceler<LocalDateTime> {
    override fun create(parcel: Parcel): LocalDateTime {
        val date = parcel.readString()
        return date?.toLocalDateTime()
            ?: LocalDateTime(0, 0, 0, 0, 0)
    }

    override fun LocalDateTime.write(parcel: Parcel, flags: Int) {
        parcel.writeString(this.toString())
    }
}

in iosMain/.../Platform.kt

import kotlinx.datetime.LocalDateTime

// Note: no need to define CommonParcelize here (bc its @OptionalExpectation)
actual interface CommonParcelable  // not used on iOS

// Note: no need to define CommonTypeParceler<T,P : CommonParceler<in T>> here (bc its @OptionalExpectation)
actual interface CommonParceler<T> // not used on iOS
actual object LocalDateTimeParceler : CommonParceler<LocalDateTime> // not used on iOS

in ../shared/commonMain/.../domain/note/Note.kt

This is your domain class that will be shared by both iOS and Android

import kotlinx.datetime.LocalDateTime

@CommonParcelize
data class Note(
    val id: Long?,
    val title: String,
    val content: String,
    val colorHex: Long,

    @CommonTypeParceler<LocalDateTime, LocalDateTimeParceler>()
    val created: LocalDateTime,
): CommonParcelable {

    companion object {
        private val colors = listOf(RedOrangeHex, RedPinkHex, LightGreenHex, BabyBlueHex, VioletHex)
        fun generateRandomColor() = colors.random()
    }
}

I tried to implement the @RawValue but its not documented (AFAIK) and the above method of using @TypeParcelers works very well for any particular class. I leave that as an exercise for someone else!

Sample project: https://github.com/realityexpander/NoteAppKMM

RealityExpander
  • 133
  • 1
  • 8
2

You need an empty actual interface for Parcelable in iOS. I don't know why it's giving you the error for JVM, as it's not in your targets. I walk through how to use Parcelable and @Parcelize in common code here.

Brady
  • 63
  • 4