この記事は2018年Kotlin Advent Calendar の22日目の記事です。
今年はAdvent Calendarに空きがあったので、代打で既に書いた記事があるので興味のある方は見てみて下さい
この2つは概念的な内容ですが、この記事は実践的な内容となっていて、どちらかというとTipsです。ただ、gradleの構成等は参考になるのではないかなと思います。
はじめに
クライアントアプリで利用されるSerializerにはKotlin SerializationとParcelize Annotationがあります。
この2つの目的(Kotlin Serializeation: JSON Serializer, Parcelize: Android Parcelable)は異なるのですが、やりたい事(Selialize)は一緒です。
お互いが干渉するものでは無いのですが、Kotlin Multiplatform環境でAndroidアプリを作る場合はどちらも利用することになると思います。
この記事では、Kotlin SerializationとParcelize Annotationを共存させる方法を解説します。
Kotlin Serializationとは
Kotlin Serialization
とは、Kotlinで作られたSerializerで、クラスに@Serializable
Annotationを付けると自動でSerializeしてくれます。
現在はJSON, CBOR, Protobufをサポートしていて、Kotlin/JVM, Kotlin/JS, Kotlin/Nativeでも利用可能なため、Kotlin Multiplatform環境で利用するSerializerとしてはデファクトスタンダードになっています。
丁度2日前のKotlin Advent Calendarの記事
でも解説されているので、使い方はそちらを参考にすると良いと思います。
Kotlin Parcelize Annotationとは
Parcelize Annotationを説明する前に、Parcelable
の説明をする必要があります。
Parcelableとは、Android OSのAPIで用意されているSerializerの一種で、Andoridでは画面間でデータを共有する場合には必ずParcelableを実装しなければなりません。
実際のinterfaceは以下のようになっていて、
interface Parcelable {
int describeContents();
void writeToParcel(Parcel dest, int flags);
}
意外とシンプルなのですが直列に値がSerializeされていくので、Deselializeの際にデータの復元順番が異なると正しくデータが復元されません。そのため、プロパティを追加する毎に正しい順番でParcelableの実装も行う必要があり、かなり面倒な仕様になっています。
// AとBの順番は一致する必要がある
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)
}
}
}
以前からこの問題を解決するライブラリはいくつか存在していたのですが、1年程前からKotlinでKotlin Andorid Extensions
としてParcelize
AnnotationをつければParcelableの実装を書かなくても良くなりました。
@Parcelize
data class User(
val id: Int,
val name: String,
val email: String
) : Parcelable
便利さが段違いですね。AndroidでDomain Objectを定義する場合にParcelable Supportを使わない選択肢はありません。 前の記事 でも説明した通り、Kotlin Multiplatform環境ではDomain Objectを各プラットフォームで共通化したいと思っています。 その場合、共通のモジュールにDomain Objectを置く必要があるのでAndroid Parcelize Annotationを呼び出す事は出来ません。
この記事では、共通のモジュールからAndroid Parcelize Annotationを呼び出す方法を書きたいと思います。
今回は、Android Parcelize Annotationですが、各プラットフォーム毎に異なるAnnotationを利用したい場合にも有効な手法ですので、やり方を知っておいて損はないと思います。
手順
今回のサンプルリポジトリはこちら にあります。 Kotlin Multiplatform環境の参考にもなると思うので見てみて下さい。
構成
このサンプルのパッケージ構成はこの様になっています。
Kotlin Multiplatformで利用される共通のモジュール名は common
になっていて、今回共通化を行いたいDomain ObjectはCommonモジュールにあるUser
クラスになります。
CommonモジュールはAndroid, iOS, Web, Serverで利用予定なので、Android, Native, JS, JVMでbuildしています。
なお、依存関係のバージョンをrootディレクトリにある dependencies.gradle
にて解決しているので、各gradleファイルでの参照先はこちらに記述されていることに注意して下さい。(依存関係の運用法は試行錯誤中です)
.
├── 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
Serializationは本家のREADME が詳しく書かれているので、本記事で深掘りはしません。
rootディレクトリのgradleファイルに以下を追記します。
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"
// 追記
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
// 追記
maven { url "https://kotlin.bintray.com/kotlinx" }
}
}
commonモジュールのbuild.gradleに追記
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
}
}
}
}
コード側で利用
gradleの設定は以上です。gradleの設定が済んだらAnnotationをクラスに追加してあげるだけで、動作します。
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: Int,
val name: String,
val email: String
)
Android Parcelable Support
common/commonMain
単純に考えれば以下のように、commonモジュールで定義したUserクラスにParcelize
Annotationを追加するだけで利用できるように思えますが、commonで定義される箇所にプラットフォーム依存のコードを含めることは出来ません。
なぜなら、commonモジュールではプラットフォーム固有のコードはimportすることが出来ないからです。
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
common/andoridMainをAndroid Library化する
Kotlin Multiplatform Libraryでも通常のAndorid Libraryと同じようにcom.android.library
を読み込む必要があります。
build.gradleとは分けるためにandroid.gradle
を用意します。このファイルは通常のAndroid Libraryと同じ形式です。一部抜粋すると以下のようになっています。
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
}
ポイントは3つあります。
- A
source sets
でディレクトリ構成を変更しています。Android Libraryは必ずAndroid.manifestファイルがmainディレクトリ配下に必要になります。そのためデフォルトだとandroidMain配下のディレクトリにmainパッケージを追加しなければならず、他のパッケージ構成とズレてしまうため、パッケージ構成の変更を行っています。この部分は好みの問題ですので、各々で決めて下さい。
- B
experimentalの機能を利用しているので記述が必要です。
- C
Androidに関する依存関係はcommon/build.gradle内のdependenciesではなく、こちらに記述していきます。
common/build.grade でandroid.gradleをimport
apply from: 'android.gradle'
expect Annotationを用意
Androidの依存関係が解決できたので、実装に入ります。
前述の様なプラットフォーム依存が発生した時のために、Kotlin Multiplatformにはexpect
とactual
というabstract
とoverride
の様な関係の修飾子を利用することが出来ます。
これを利用して、Kotin MultiplatformにおいてもAndroid Parcelize Annotationを実装していきます。
Kotlin Parcelize Annotation
用にcommonモジュールにexpect Annotationを追加します
@UseExperimental(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class AndroidParcelize()
一番上のAnnotation @UseExperimental
はKotlin1.3の新機能です。以前記事を書いた
のでそちらを参照して下さい。
下2つのAnnotationは、Parcelizeと同様のものを付けています。
今回のポイントは OptionalExpectation
Annotationです。
先程expect
とactual
はabstract
とoverride
の様な関係と述べました。つまり、commonモジュールに書いたexpectは各プラットフォームでactual実装を行わなければなりません。しかし、OptionalExpectationを利用すればactual実装を行わなくてもコンパイルエラーにはならなくなります。
普段のコードで使う事は推奨出来ませんが、今回の例のように1つのプラットフォームのみ実装したい場合に非常に威力を発揮します。
Android Parcelable
用にcommonモジュールにexpect interfaceを追加します
Android ParcelableのObjectを作成するにはandorid.os.Parcelable
を実装する必要があるので、commonモジュールにinterfaceを実装します。
expect interface AndroidParcel
actualを実装
Kotlin Parcelize Annotation
用のactual
import kotlinx.android.parcel.Parcelize
actual typealias AndroidParcelize = kotlinx.android.parcel.Parcelize
ここもポイントです。
commonモジュールに定義したAndroidParcelize
Annotationに対してtypealiasを用いてactualを実装しています。
こうすることで、Andorid用に生成されるKotlin Multiplatformのコードには、独自に作成したAndroidParcelize
Annotationに対して、Kotlin Android Supportの機能が追加されます。
Android Parcelable
用のParcelable
> androidMain/actual.kt
import android.os.Parcelable
actual interface AndroidParcel : Parcelable
> others
actual interface AndroidParcel
AndroidParcelはinterfaceで定義されているので、そのままandroid.os.Parcelable
をプロキシしてあげましょう。他のプラットフォームに関しては何も意味を持たないので、空実装で問題ありません。
Android側から使う
簡単な例だとこのようになります。
val user = User(100, "AAkira", "hoge@gmail.com")
startActivity(
Intent(this, MainActivity::class.java).apply {
putExtra("user", user)
}
)
問題なく、AndroidのモジュールからもParcelableとして認識されています。
SelializationとParcelizeの共存
共存といっても、Annotationを2つ追加するのみです。これで問題なく動作します。
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
まとめ
以上の設定で、Kotlin SerializationとParcelize AnnotationをKotlin Multiplatform環境で動作させることが出来ました。このやり方はJetBrainsの人に直接聞いたやり方なので、調べた限りオンライン上にはまだ例が上がっていないので参考になるかなと思って書きました。 ~英語化した方がいい内容な気がするので気が向いたら英語化したいですが、おそらく気は向きませんw~
海外からの問い合わせがあったので英語化しました!
Use the Kotlin serialization and Android percelable on Kotlin MPP
今回のサンプルリポジトリはこちらです。
https://github.com/AAkira/KotlinMultiplatformAndoridParcelize