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?

【Kotlin】「Reified + Sealed + Generic」で設計する 型安全な Routing DSL

Posted at

はじめに

NavigationやScreen遷移を「文字列やID」ではなく型で保証するルーティング設計


1. 従来の問題:文字列ベースルーティングの限界

例えば、よくある Navigation の実装:

navController.navigate("detail/123")

問題点:

  • "detail/123"文字列依存で型安全性ゼロ
  • 引数の数・型・順序が保証されない
  • 画面遷移が増えると「typo」「バージョン不整合」が頻発

これを Kotlin の型システムを活かして、
コンパイル時にルートの整合性を保証できる仕組みを作ります。


2. Reified + Sealed + Generic の基本構成

コアアイデア:

  • sealed class Route<T>「行き先と型」 を定義
  • inline + reified実行時にも型を保持
  • RouteBuilder DSL で 宣言的に遷移表現

3. 型安全な Route 定義

sealed class Route<T>(
    val path: String
) {
    data object Home : Route<Unit>("home")
    data class Detail(val id: Int) : Route<Detail>("detail/$id")
    data class Profile(val userId: String) : Route<Profile>("profile/$userId")
}

特徴:

  • sealed class により、定義済みルート以外はコンパイルエラー
  • 各 Route が自分の引数を保持(id, userId など)
  • 型パラメータ <T> により 結果型や引数型を結びつけられる

4. RouteBuilder DSL の実装

@DslMarker
annotation class RouteDsl

@RouteDsl
class RouteBuilder {
    private val routes = mutableListOf<Route<*>>()

    fun route(route: Route<*>) {
        routes += route
    }

    fun build(): List<Route<*>> = routes
}

fun navigation(block: RouteBuilder.() -> Unit): List<Route<*>> =
    RouteBuilder().apply(block).build()

利用例:

val appRoutes = navigation {
    route(Route.Home)
    route(Route.Detail(42))
    route(Route.Profile("anna"))
}

結果:

[Route.Home, Route.Detail(id=42), Route.Profile(userId=anna)]

5. Navigation DSL の組み立て(with reified)

今度は、reified 型を使って安全に遷移を実行します。

inline fun <reified R : Route<*>> navigateTo(route: R) {
    println("Navigating to ${route.path} (${R::class.simpleName})")
}

利用:

navigateTo(Route.Detail(10))
navigateTo(Route.Profile("Anna"))

出力:

Navigating to detail/10 (Detail)
Navigating to profile/Anna (Profile)
  • R::class.simpleName で実行時の型が利用可能
  • 未定義のルートは sealed class により 定義漏れを防止

6. 型付き引数と戻り値を持つルート

次に、「遷移引数と戻り値」を型安全に定義してみます。

sealed interface RouteSpec<Arg, Result> {
    val path: String
}

object HomeRoute : RouteSpec<Unit, Unit> {
    override val path = "home"
}

data class DetailRoute(val id: Int) : RouteSpec<Int, String> {
    override val path get() = "detail/$id"
}

そして reified × generic なナビゲーションAPI:

inline fun <reified Arg, reified Result> navigate(
    route: RouteSpec<Arg, Result>,
    arg: Arg,
    onResult: (Result) -> Unit
) {
    println("Navigate → ${route.path} with arg=$arg")
    // 実際には Intent や NavHost を呼ぶ
    val mockResult: Result = "OK" as Result
    onResult(mockResult)
}

使用例:

navigate(DetailRoute(42), 42) { result ->
    println("Got result: $result")
}

出力:

Navigate → detail/42 with arg=42
Got result: OK

ArgResult型パラメータでバインドされているため、
引数・戻り値のミスマッチがコンパイル時に検出されます。


7. Compose / Android / KMM への応用

Jetpack Compose に統合

@Composable
fun NavGraphDSL() {
    val navController = rememberNavController()

    NavHost(navController, startDestination = Route.Home.path) {
        composable(Route.Home.path) { HomeScreen(navController) }
        composable("detail/{id}") { backStack ->
            val id = backStack.arguments?.getString("id")?.toInt() ?: 0
            DetailScreen(id)
        }
    }
}

→ Route オブジェクトを使うことで、

  • Route.Detail(10).path"detail/10"
  • NavigationGraph と定義が 1箇所で同期

Kotlin Multiplatform (KMM)

  • RouteSpec<Arg, Result> は共通コードに置ける
  • Android / iOS 両方で同じ型安全DSLを共有可能
  • AndroidではIntentに、iOSではCoordinatorにマッピング可能

8. 設計パターンまとめ

要素 概念 効果
sealed class ルートの有限集合化 定義漏れ・typo防止
reified 実行時型保持 DSLで R::class 参照可能
generic Arg/Result の型束縛 引数と戻り値の整合性を保証
@DslMarker スコープ安全 ネストした DSL の誤参照防止
RouteBuilder 宣言的構築 NavigationMap の組み立てを簡略化
RouteSpec<Arg, Result> 汎用プロトコル化 Compose / Intent / iOS 共有可能

まとめ

技術 役割
Reified 実行時型の取得を可能にする
Sealed 限定的なルート定義を強制
Generic Arg / Result の型をバインド
DSL構文 宣言的で誤りに強いルーティング定義

Kotlin DSL の力で、ルーティングも「文字列操作」から「型システムに守られた設計」へ。

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?