🕌

Android: kotshi から moshi-kotlin-codegen に移行する

2022/11/01に公開

moshi の kotlin 用ライブラリ kotshi に依存しているのを消したいなーと思いまして、その時の対応メモです。

単純に kotshli をなくしてもいいのですが、それだと、moshi-kotlin による動的なクラスロードに依存してしまい重くなる(らしい)。
moshi 公式で moshi-kotlin-codegen というコンパイル時にkaptで解決してくれるライブラリがあり、これに置き換えれば万事解決しそうです。

手順

  1. build.gradle で debugImplementation moshi-kotlin / kaptRelease moshi-kotlin-codegen にする。
  2. @se.ansman.kotshi.JsonSerializable@com.squareup.moshi.JsonClass(generateAdapter = true) に変更する。 @field:Json@Json に置き換える。
  3. デバッグ時のみ、 KotlinJsonAdapterFactoryMoshi.Builder() に追加する。

1.build.gradleの設定変更

変更前

    val kotshiVersion = "2.6.3"
    implementation("se.ansman.kotshi:api:$kotshiVersion")
    kapt("se.ansman.kotshi:compiler:$kotshiVersion")
    val moshiVersion = "1.12.0"
    implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion")

変更後

moshi 1.12 は kotlin 1.6系で kaptビルドエラーになる不具合があるため、 moshi1.13 にアップデートしました。

    val moshiVersion = "1.13.0" // kotlin1.6でのmoshi不具合解消のためアップデート
    implementation("com.squareup.moshi:moshi:$moshiVersion")
    // デバッグ時はビルド速度優先で moshi-kotlinを使い、リリース時は実行パフォーマンス向上のため moshi-kotlin-codegen を使う
    debugImplementation("com.squareup.moshi:moshi-kotlin:$moshiVersion")
    kaptRelease("com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion")

2. JSONシリアライズ・デシリアライズ対象クラスのアノテーションを設定

変更前

import com.squareup.moshi.Json
import se.ansman.kotshi.JsonSerializable

@JsonSerializable
data class User(
     @field:Json(name = "user_id")
     val id: Int,
     val name: String,
)

変更後

proguard の難読化を避けるため、 generateAdapter = true とします。
また、@field:Json は解釈されないため、 @Json に変更しました。

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class User(
     @Json(name = "user_id")
     val id: Int,
     val name: String,
)

3. Moshi.Builder の設定変更

kotshi用のApplicationJsonAdapterFactoryを削除しました。
また、moshi-kotlin-codegenに存在しない KotlinJsonAdapterFactory を動的ロードに変更してビルドエラーを避けています。

変更前

	return Moshi.Builder()
	    .add(ApplicationJsonAdapterFactory()) // kotshi用(削除)
	    .add(KotlinJsonAdapterFactory()) // moshi-kotlin用
	    .build()

変更後

  • デバッグ時だけ moshi-kotlin を使う
        val builder = Moshi.Builder()
        if (BuildConfig.DEBUG) {
            // デバッグ時は moshi-kotlin を使う。
            val instance = Class.forName("com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory").newI
nstance() as JsonAdapter.Factory
            builder.add(instance)
        }
	return builder.build()
  • 別解: クラスロードできるときだけmoshi-kotlinを使う。
    gradleだけで設定切り替えできるのでこちらのほうが便利。
    ただし、SDKでの提供時など、外部モジュールが moshi-kotlinをロードする可能性がある場合は引きずられてしまうことに注意。
        val builder = Moshi.Builder()
	// moshi-kotlin 対応時は KotlinJsonAdapterFactory を差し込む。
        try {
            val factory = Class.forName("com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory").newInstance() as JsonAdapter.Factory
            builder.add(factory)
        } catch (_: ClassNotFoundException) {
        }

        return builder.build()

余談。はまった所

すでに kapt を使っているモジュールのgradleで、 kotlin("kapt") を使ったらリリースビルド時だけJSON解決できない→Retrofit2 でAPIコールできない、というので丸一日ハマりました。

結論としては、 kotlin("kapt")id("kotlin-kapt") はメンテナンスされてなくて古いので、
id("org.jetbrains.kotlin.kapt") を使いましょう。という話でした。
kotlin-なんとか みたいなプラグインの命名規則がわかりづらく、mavenの group:artifact:version 表記にしましょう、ということのようで・・・。

参考: build.gadle* に 'android-library' や 'kotlin-kapt' があったらやばい!

具体的には hiltのkaptがすでにあるモジュールの build.gradle.kts で、

    id("com.android.application")
    kotlin("android")
    kotlin("kapt")
    id("dagger.hilt.android.plugin")
}

となっていて、

val jsonAdapter = moshi.adapter(VerificationInfoEntity::class.java)
Log.d(TAG, "jsonAdapter=$jsonAdapter")

としても、リリースビルド時だけ動かない。

kaptRelease("com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion")

でなく

kapt("com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion")

としてデバッグビルド時もkapt有効にしても問題なく動く。minifyも関係ない。という謎現象に苦しんでました。

kotlin("kapt") とか id("kotlin-kapt")
kotlin("android") とか id("kotlin-android") のような、古い書式のpluginは使わないようにしよう、と心に誓ったドハマリでした…。

Discussion