This article is translated Kotlin Multiplatform環境でKotlin SerializationとAndroid ExtensionsのParcelize Annotationを使う from Japanese to English.
First
There are Kotlin serialization and Parcelize annotation in serializer used by client application.
These two purposes (Kotlin Serialize: JSON Serializer, Parcelize: Android Parcable) are different, but what you want to do (Selialize) is same.
There is no interference with each other, but I think that both of them will be used to create Android apps in the Kotlin multiplatform environment.
In this article, I will explain how to make Kotlin serialization coexist with Parcel annotation.
What is the kotlin serialization
Kotlin Serialization
is serializer and written in Kotlin. It serializes if you add the @Serializable
annotation on your class.
It supports the JSON, CBOR and Protobuf. Moreover, it supports the Kotlin/JVM, Kotlin/JS, Kotlin/Native. Therefore, it is generally used as a serializer in the kotlin multiplatform project.
What is the kotlin parcelize annotation
I need to explain the parcelable
before the parcelize annotation.
It is a kind of serializer that is prepared the api of android os.
You must implement the Parcelable to share data between screens on Android.
It is as follows
interface Parcelable {
int describeContents();
void writeToParcel(Parcel dest, int flags);
}
It is very simple but the data will not be restored correctly if the data restoration order is different during deserialization. Therefore, it is necessary to implement parcable in the correct order each time you add properties, which is a cumbersome specification.
// The order of A and B needs to match
data class User(
val id: Int,
val name: String,
val email: String
) : Parcelable {
constructor(parcel: Parcel) : this(
// A
parcel.readInt(),
parcel.readString(),
parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
// B
parcel.writeInt(id)
parcel.writeString(name)
parcel.writeString(email)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<User> {
override fun createFromParcel(parcel: Parcel): User {
return User(parcel)
}
override fun newArray(size: Int): Array<User?> {
return arrayOfNulls(size)
}
}
}
There are a lot of libraries solved this problem.
About 1 year ago, it is not necessary to implement a selializer and deserializer if you write a Parcelize
annotation as Kotlin Andorid Extensions
.
@Parcelize
data class User(
val id: Int,
val name: String,
val email: String
) : Parcelable
This is very useful. There is no option not to use parcable support when defining Domain objects on Android. As I wrote this article[JP] before, I want to make Domain objects common on each platform. In this case, you cannot call the Parcelize annotation because you need to put Domain object in common module. I’ll write how to call the Android parcelize annotation from common module. I will write about Android parcelize annotation in this time, but it’s also a useful technique if you want to use different annotations for each platform.
Process
This is sample repository.
Structure
The package configuration of this sample is as follows.
The common module name used by Kotlin multiplatform is common
, and the Domain object that you want to common is the User
class.
The common module will be used on Android, iOS, Web and Server, therefore it is referenced by Android, Native, JS and JVM.
Note that the dependency version is resolved in dependencies.gradle
in the root directory, therefore references in each gradle file are described here. (The operation of dependency is under trial and error)
.
├── KotlinMppAndroidParcelable.iml
├── README.md
├── android
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ └── github
│ │ └── aakira
│ │ └── kotlinnativesample
│ │ └── MainActivity.kt
│ └── res
│ ├── drawable
│ └── values
├── build.gradle
├── common
│ ├── android.gradle
│ ├── build.gradle
│ └── src
│ ├── androidMain
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── aakira
│ │ └── kotlinnativesample
│ │ └── common
│ │ └── actual.kt
│ ├── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── aakira
│ │ └── kotlinnativesample
│ │ └── common
│ │ ├── common.kt
│ │ └── model
│ │ └── User.kt
│ ├── iosMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── aakira
│ │ └── kotlinnativesample
│ │ └── common
│ │ └── actual.kt
│ ├── jsMain
│ │ └── kotlin
│ │ └── com
│ │ └── github
│ │ └── aakira
│ │ └── kotlinnativesample
│ │ └── common
│ │ └── actual.kt
│ └── jvmMain
│ └── kotlin
│ └── com
│ └── github
│ └── aakira
│ └── kotlinnativesample
│ └── common
│ └── actual.kt
├── dependencies.gradle
├── gradle
├── gradle.properties
├── gradlew
├── gradlew.bat
├── ios
└── settings.gradle
Kotlin Serialization
I don’t describe the Serialization in this article because readme is written in detail.
build.gradle in root directory
buildscript {
ext.kotlin_version = '1.3.11'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// add
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
// add
maven { url "https://kotlin.bintray.com/kotlinx" }
}
}
build.gradle in common directory
apply plugin: 'kotlin-multiplatform'
apply plugin: 'kotlinx-serialization'
kotlin {
sourceSets {
commonMain {
dependencies {
// serialization
implementation rootProject.ext.serializationCommon
}
}
androidMain {
dependencies {
// serialization
implementation rootProject.ext.serialization
}
}
iosMain {
dependencies {
// serialization
implementation rootProject.ext.serializationNative
}
}
jsMain {
dependencies {
// serialization
implementation rootProject.ext.serializationJs
}
}
jvmMain {
dependencies {
// serialization
implementation rootProject.ext.serialization
}
}
}
}
Usage
Setting of gradle is above. just add an annotation to your class and it works after you have set up gradle.
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: Int,
val name: String,
val email: String
)
Android Parcelable Support
common/commonMain
It seems that it can be used by adding Parcelize
annotation to the User class defined in the common module as follows simply, but platform dependent code can not be included in the common module because platform-specific code can not be imported in the common module.
import android.os.Parcelable // cannot import
import kotlinx.android.parcel.Parcelize // cannot import
@Parcelize
data class User(
val id: Int,
val name: String,
val email: String
) : Parcelable
Make an Android library for common/andoridMain
You need to load com.android.library
in Kotlin multiplatform library as well as regular andorid library.
Therefore, you prepare android.gradle
to separate from build.gradle. This file has the same format as regular android library. The excerpt is as follows.
apply plugin: 'com.android.library'
apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-android-extensions'
android {
// A
sourceSets.each {
def root = "src/androidMain/${it.name}"
it.setRoot(root)
it.java.srcDirs += "${root}/kotlin"
it.manifest.srcFile "src/androidMain/AndroidManifest.xml"
}
// B
androidExtensions {
experimental = true
}
}
// C
dependencies {
implementation rootProject.ext.kotlin
// serialization
implementation rootProject.ext.serialization
}
There are three points.
[A]. It changes the directory structure at source sets
.
An android library always requires Android.manifest file under the main directory.
Therefore, you usually have to add the main package to the directory under androidMain.
The package configuration has been changed because it deviates from other package configurations.
This is matter of taste, therefore please decide each one.
[B]. It needs to be described because it uses experimental functions.
[C]. The dependencies for android will be described here instead of the dependencies in common/build.gradle
.
import the android.gradle at common/build.grade
apply from: 'android.gradle'
prepare an expect annotation
The android dependency has been resolved, we can get into the implementation.
The Kotlin multiplatform can use modifiers such as expect
and actual
like abstract
and override
, in case of platform-dependent problems as described avove.
We will use this to implement the android parcelize annotation on kotin multiplatform.
- Add the expect annotation in common module for kotlin parcelize annotation
@UseExperimental(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class AndroidParcelize()
The top Annotation is a new feature of Kotlin 1.3.
I wrote an article[jp]
before, so please refer to that.
The following two Annotations are similar to Parcelize.
The point is OptionalExpectation
annotation.
Earlier I wrote expect
and actual
as a relationship between abstract
and override
.
In other words, expect
written in the common module must have actual implementation on each platform.
However, if you use OptionalExpectation, you won’t get compilation errors without actual
implementation.
It is not recommended to use it in ordinary code, but it is very useful if you want to implement only one platform as in this example.
- Add the expect interface in common module for android parcelable
Implement interface in common module because you need to implement android.os.Parcelable
to create an android parcelable object.
expect interface AndroidParcel
implements actual
- This is actual implementation for Kotlin parcelize annotation
import kotlinx.android.parcel.Parcelize
actual typealias AndroidParcelize = kotlinx.android.parcel.Parcelize
This is also a point.
Implements actual
using typealias to AndroidParcelize
annotation defined in common module.
As a result, the Kotlin multiplatform code generated for Andorid adds the Kotlin android support functionality to the AndroidParcelize
annotation created above.
- Android parcelable
> androidMain/actual.kt
import android.os.Parcelable
actual interface AndroidParcel : Parcelable
> others
actual interface AndroidParcel
AndroidParcel
is defined in interface, so it just proxy android.os.Parcelable
.
There is nothing wrong with the empty implementation, as it has no meaning for other platforms.
Use parcelable by android
This is a simple example.
val user = User(100, "AAkira", "hoge@gmail.com")
startActivity(
Intent(this, MainActivity::class.java).apply {
putExtra("user", user)
}
)
The android module recoginizes as Parcelable.
Use the Selialization and Parcelize at the same time
It only adds two annotations at the same time.
The following example works.
import com.github.aakira.kotlinnativesample.common.AndroidParcel
import com.github.aakira.kotlinnativesample.common.AndroidParcelize
import kotlinx.serialization.Serializable
@Serializable
@AndroidParcelize
data class User(
val id: Int,
val name: String,
val email: String
) : AndroidParcel
Summary
You are now able to run the Kotlin serialization and the Parcel annotation in the Kotlin multiplatform environment. I heard this method directly to JetBrains people, so I think that it will be helpful as I have not found an example online yet.
This is sample repository.
https://github.com/AAkira/KotlinMultiplatformAndoridParcelize