0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Android]MoshiからKotlinx Serializationへ — APIパースとNavigationをまとめて整理する

0
Posted at

1. はじめに

これまで個人の学習用プロジェクトでは、JSONのパースに Moshi、Navigationには 文字列ルート + sealed class という構成を使っていました。動作自体に問題はなかったのですが、画面を追加するたびに "detail/{id}" のような文字列ルートと navArgument のボイラープレートを繰り返し書く必要がある、という不満がありました。

現在の業務プロジェクトではすでに kotlinx.serialization を採用しており、実際に使ってみると Navigation Compose 2.8+Type-Safe Navigation まで同じライブラリで自然に統合できる点が印象的でした。そこで今回、学習用プロジェクトにも同じ構成を反映し、JSONパースとNavigationを1つのライブラリに統一するマイグレーションを行いました。

この記事では、次の2点を整理します。

  • なぜ最近は Kotlinx Serialization が使われるのか(Gson・Moshiとの比較)
  • 実プロジェクトで Moshi → Kotlinx Serialization文字列ルート → Type-Safe Navigation へ移行した過程

2. Gson / Moshi / Kotlinx Serialization は何が違うのか

AndroidのJSONパースライブラリは大きく3つが使われてきました。それぞれの特徴を先に押さえておくと、なぜ今 kotlinx.serialization なのかが自然に理解できます。

1) Gson(2008〜)

Googleが作ったライブラリで、Android初期から標準のように使われてきました。

data class User(val id: Int, val name: String)

val gson = Gson()
val user = gson.fromJson(json, User::class.java)

課題

  • リフレクションベース — 実行時にクラス情報を読み取ってフィールドを埋めるため遅く、R8/ProGuard使用時はルールの追加が必須
  • Kotlinのnull safetyを無視val name: String(non-null)でもJSONに name が無ければ null が入ってしまう
  • Kotlinのデフォルト値を無視val age: Int = 20 と宣言してもJSONに無ければ 0 が入る
  • コンストラクタを呼ばない — リフレクションでオブジェクトを生成してフィールドを埋めるだけなので、init ブロックの検証ロジックが走らない

2) Moshi(2016〜)

Square(Retrofitの開発元)が作ったライブラリ。GsonのKotlin周りの弱点を補うために登場しました。

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

改善点

  • Kotlinのnull safetyを認識 — non-nullフィールドが欠けているとパース失敗として明示的にエラーになる
  • コード生成(KSP)方式に対応 — リフレクション無しでコンパイル時にアダプタを生成 → 高速でR8フレンドリー
  • デフォルト値を尊重 — Kotlinのコンストラクタのデフォルト値が正しく使われる

残る課題

  • デフォルトモードは依然としてリフレクションベースKotlinJsonAdapterFactory を使うとGson同様 kotlin-reflect に依存する。リフレクションを避けるには @JsonClass(generateAdapter = true) + KSPのコード生成モードを別途設定する必要がある
  • 追加の依存関係とKSPプロセッサが必要(moshi-coremoshi-kotlinmoshi-kotlin-codegen
  • JVM専用 — Android/Java環境でしか動かない

3) Kotlinx Serialization(2020〜)

JetBrainsが自ら開発したKotlin公式ライブラリです。アノテーションプロセッサではなく、Kotlinコンパイラプラグインとして動作します。

@Serializable
data class User(val id: Int, val name: String)

主な差別化ポイント

  • リフレクションでもKSPでもない — コンパイラが直接シリアライズコードを生成する。最も速く、軽い
  • Kotlin Multiplatform対応 — Android、iOS、どこでも同じように動作する
  • Kotlin公式 — 言語機能(sealed class、value class、デフォルト値など)と100%互換
  • Navigation Compose 2.8+ のType-Safe Navigationがこのライブラリを使用 → ルーティングとAPIパースを1つのライブラリに統合できる

📊 ひと目で比較

項目 Gson Moshi Kotlinx Serialization
リリース 2008 2016 2020
開発元 Google Square JetBrains(Kotlin公式)
動作方式 リフレクション リフレクション または KSPコード生成 コンパイラプラグイン
Kotlin null safety
Kotlinデフォルト値
sealed class対応 △(アダプタを自作) ✅(ネイティブ)
Multiplatform ❌(JVM専用) ❌(JVM専用)
R8 / ProGuardフレンドリー ❌(ルール必要) △(codegenモードのみ)
Navigation Compose連携

🎯 なぜ最近はKotlinx Serializationなのか

整理すると、理由は次の3点に集約されます。

  1. Kotlin公式ライブラリ — 言語機能と常に同期し、JetBrainsがメンテナンスしている
  2. コンパイラプラグイン方式 — リフレクションでもKSPでもない、効率的なコード生成方式
  3. エコシステムの広がり — Navigation ComposeやKtorなどがKotlinx Serializationを標準として採用。1つのライブラリでより広い領域をカバーできる

3. Before — Moshi + 文字列ルート

(1) Moshiの設定

NetworkModule.kt
@Provides
@Singleton
fun provideMoshi(): Moshi =
    Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .build()

@Provides
@Singleton
fun provideRetrofit(moshi: Moshi, okHttpClient: OkHttpClient): Retrofit =
    Retrofit.Builder()
        .baseUrl("...")
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .client(okHttpClient)
        .build()

(2) 文字列ベースのNavigation

Routes.kt
sealed class Screen(val route: String) {
    data object Home : Screen("home")
    data object Detail : Screen("detail/{id}") {
        fun createRoute(id: Int) = "detail/$id"
        const val ID_ARG = "id"
    }
}

composable(
    route = Screen.Detail.route,
    arguments = listOf(
        navArgument(Screen.Detail.ID_ARG) { type = NavType.IntType }
    )
) { backStackEntry ->
    val id = backStackEntry.arguments?.getInt(Screen.Detail.ID_ARG) ?: return@composable
    DetailScreen(id = id, ...)
}

このコードの問題点

  • "detail/{id}" のような文字列ルートはタイプミスをしてもコンパイラが検出してくれない
  • navArgument + NavType のボイラープレートが画面ごとに繰り返される
  • パラメータ取得時に arguments?.getInt(...) ?: return のようなnullable処理が必要

4. After — Kotlinx Serializationの適用

(1) Gradle依存関係の変更

libs.versions.toml
[versions]
kotlinx-serialization = "1.7.3"

[libraries]
retrofit-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.serialization)
}

dependencies {
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.retrofit.converter.kotlinx.serialization)
}

(2) DTOに @Serializable を付与

@Serializable
data class MessageDto(
    val id: Int,
    val title: String,
    // ...
)

(3) Retrofit Converterの差し替え

NetworkModule.kt
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit =
    Retrofit.Builder()
        .baseUrl("...")
        .addConverterFactory(
            Json { ignoreUnknownKeys = true }
                .asConverterFactory("application/json; charset=UTF-8".toMediaType())
        )
        .client(okHttpClient)
        .build()

MoshiインスタンスのProviderが不要になり NetworkModule がかなり軽くなりました。また ignoreUnknownKeys = true を指定することで、サーバー側で新しいフィールドが追加されてもアプリが壊れない防御的な実装にできます。

ignoreUnknownKeys はデフォルトで false です。未知のキーが来ると例外になるため、実サービスのAPIを扱う場合は明示的に true にしておくのが無難です。

(4) Type-Safe Navigation

Routes.kt
@Serializable
data object Home

@Serializable
data class Detail(val id: Int)

NavHost(navController = navController, startDestination = Home) {
    composable<Home> {
        HomeScreen(onItemClick = { id -> navController.navigate(Detail(id)) })
    }
    composable<Detail> { backStackEntry ->
        val detail: Detail = backStackEntry.toRoute()
        DetailScreen(id = detail.id, ...)
    }
}

@Serializable を付けたクラス自体がルートになり、navArgumentNavType、文字列ヘルパーがすべて不要になりました。

5. 何が良くなったか

項目 Before After
JSONパースライブラリ Moshi Kotlinx Serialization
コンパイラサポート KSPによるアダプタ生成 コンパイラプラグイン
Navigationルート 文字列("detail/{id}" 型安全なオブジェクト(Detail(id)
パラメータ取得 arguments?.getInt(...) backStackEntry.toRoute()
Routes.kt のコード量 38行 27行(-29%)

特にNavigation側の変化が体感として一番大きかったです。ルート定義 → 登録 → パラメータ取得までがすべて1つの型安全なオブジェクトで完結するため、新しい画面を追加するときに気にすることが半分になりました。

6. おわりに

JSONパースとNavigationは一見無関係に見えますが、どちらも 「オブジェクト ↔ 文字列」のシリアライズという共通点があります。Kotlinx Serializationを導入することで、この2つの領域のシリアライズ方式を1つのライブラリに統一でき、結果としてコード量が減り、型安全性が上がりました。

特にNavigation Compose 2.8+ のType-Safe NavigationはKotlinx Serializationを前提に設計されているため、新規プロジェクトであれば最初からKotlinx Serializationで始めるのが一番きれいな選択だと感じました。

実際のマイグレーションコミット: GEUN-TAE-KIM/Mvi_Orbit_Study @ 41ca94f

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?