April 1, 2019

Use the Kotlin serialization and Android percelable on Kotlin MPP

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

© AAkira 2023