Skip to content

ナビゲーションとルーティング

ナビゲーションは、ユーザーがアプリケーションの異なる画面間を移動できるようにする、UIアプリケーションの主要な部分です。 Compose Multiplatformは、Jetpack Composeのナビゲーション手法を採用しています。

セットアップ

Navigationライブラリを使用するには、commonMainソースセットに以下の依存関係を追加します。

kotlin
kotlin {
    // ...
    sourceSets {
        // ...
        commonMain.dependencies {
            // ...
            implementation("org.jetbrains.androidx.navigation:navigation-compose:2.9.1")
        }
        // ...
    }
}

サンプルプロジェクト

Compose Multiplatformのナビゲーションライブラリが実際に動作している様子を確認するには、nav_cupcakeプロジェクトをチェックしてください。 これは、Androidのコードラボ「Navigate between screens with Compose」から変換されたものです。 より複雑な例については、公式のKotlinConfアプリケーションを参照してください。

Jetpack Composeと同様に、ナビゲーションを実装するには以下の手順が必要です。

  1. ナビゲーショングラフに含めるルートをリストアップします。各ルートはパスを定義する一意の文字列である必要があります。
  2. ナビゲーションを管理するためのメインのcomposableプロパティとして、NavHostControllerインスタンスを作成します。
  3. アプリにNavHost composableを追加します:
    1. 先ほど定義したルートのリストから、開始目的地(starting destination)を選択します。
    2. NavHostの作成の一部として直接、またはNavController.createGraph()関数を使用してプログラム的に、ナビゲーショングラフを作成します。

各バックスタックエントリ(グラフに含まれる各ナビゲーションルート)は、LifecycleOwnerインターフェースを実装しています。 アプリの異なる画面間の切り替えにより、状態がRESUMEDからSTARTEDへ、またその逆へと変化します。 RESUMEDは「settled(確定)」とも表現されます。新しい画面が準備されアクティブになった時点で、ナビゲーションは完了したと見なされます。 現在のCompose Multiplatformにおける実装の詳細については、Lifecycleのページを参照してください。

Webアプリにおけるブラウザナビゲーションのサポート

Compose Multiplatform for webは、共通のNavigationライブラリAPIを完全にサポートしており、ブラウザからのナビゲーション入力をアプリで受け取ることができます。 ユーザーはブラウザの 戻る ボタンや 進む ボタンを使用して、ブラウザ履歴に反映されたナビゲーションルート間を移動したり、アドレスバーを使用して現在の場所を把握したり、目的地に直接移動したりできます。

Webアプリを共通コードで定義されたナビゲーショングラフにバインドするには、Kotlin/WasmコードでNavController.bindToBrowserNavigation()メソッドを使用できます。 Kotlin/JSでも同じメソッドを使用できますが、Wasmアプリケーションが初期化され、Skiaがグラフィックスをレンダリングする準備ができていることを確実にするために、onWasmReady {}ブロックでラップしてください。 以下にセットアップの例を示します。

kotlin
//commonMainソースセット
@Composable
fun App(
    onNavHostReady: suspend (NavController) -> Unit = {}
) {
    val navController = rememberNavController()
    NavHost(...) {
        //...
    }
    LaunchedEffect(navController) {
        onNavHostReady(navController)
    }
}

//wasmJsMainソースセット
@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalBrowserHistoryApi
fun main() {
    val body = document.body ?: return
    ComposeViewport(body) {
        App(
          onNavHostReady = { it.bindToBrowserNavigation() }
        )
    }
}

//jsMainソースセット
@OptIn(ExperimentalComposeUiApi::class)
@ExperimentalBrowserHistoryApi
fun main() {
    onWasmReady {
        val body = document.body ?: return@onWasmReady
        ComposeViewport(body) {
            App(
                onNavHostReady = { it.bindToBrowserNavigation() }
            )
        }
    }
}

navController.bindToBrowserNavigation()を呼び出した後:

  • ブラウザに表示されるURLには、現在のルートが反映されます(URLフラグメントの#文字以降)。
  • アプリは手動で入力されたURLを解析し、アプリ内の目的地へと変換します。

デフォルトでは、型セーフなナビゲーションを使用する場合、目的地はkotlinx.serializationのデフォルトに従って、引数が付加されたURLフラグメントに変換されます: <app package>.<serializable type>/<argument1>/<argument2>。 例えば、example.org#org.example.app.StartScreen/123/Alice%2520Smithのようになります。

ルートからURLへの変換(およびその逆)のカスタマイズ

Compose Multiplatformアプリはシングルページアプリ(SPA)であるため、フレームワークはアドレスバーを操作して通常のWebナビゲーションを模倣します。 URLをより読みやすくし、実装をURLパターンから分離したい場合は、画面に直接名前を割り当てるか、目的地のルートに対して完全にカスタムな処理を開発できます。

  • 単にURLを読みやすくするには、@SerialNameアノテーションを使用して、シリアライズ可能なオブジェクトまたはクラスにシリアル名を明示的に設定します。

    kotlin
    // アプリのパッケージ名とオブジェクト名を使用する代わりに、
    // このルートは単に "#start" としてURLに変換されます
    @Serializable @SerialName("start") data object StartScreen
  • すべてのURLを完全に構築するには、オプションのgetBackStackEntryRouteラムダを使用できます。

URLの完全なカスタマイズ

完全にカスタムなルートからURLへの変換を実装するには:

  1. オプションのgetBackStackEntryRouteラムダをnavController.bindToBrowserNavigation()関数に渡し、必要に応じてルートをURLフラグメントに変換する方法を指定します。
  2. 必要に応じて、アドレスバーのURLフラグメントをキャッチし(ユーザーがアプリのURLをクリックまたは貼り付けたとき)、URLをルートに変換して適切にナビゲートするコードを追加します。

以下は、後述のWebコードのサンプルで使用する単純な型セーフなナビゲーショングラフの例です (commonMain/kotlin/org.example.app/App.kt):

kotlin
// ナビゲーショングラフのルート引数用のシリアライズ可能なオブジェクトとクラス
@Serializable data object StartScreen
@Serializable data class Id(val id: Long)
@Serializable data class Patient(val name: String, val age: Long)

@Composable
internal fun App(
    onNavHostReady: suspend (NavController) -> Unit = {}
) = AppTheme {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = StartScreen
    ) {
        composable<StartScreen> {
            Column(
                modifier = Modifier.fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text("Starting screen")
                // 適切なパラメータで 'Id' 画面を開くボタン
                Button(onClick = { navController.navigate(Id(222)) }) {
                    Text("Pass 222 as a parameter to the ID screen")
                }
                // 適切なパラメータで 'Patient' 画面を開くボタン
                Button(onClick = { navController.navigate(Patient( "Jane Smith-Baker", 33)) }) {
                    Text("Pass 'Jane Smith-Baker' and 33 to the Person screen")
                }
            }
        }
        composable<Id> {...}
        composable<Patient> {...}
    }
    LaunchedEffect(navController) {
        onNavHostReady(navController)
    }
}

wasmJsMain/kotlin/main.ktで、.bindToBrowserNavigation()の呼び出しにラムダを追加します。

kotlin
@OptIn(
    ExperimentalComposeUiApi::class,
    ExperimentalBrowserHistoryApi::class,
    ExperimentalSerializationApi::class
)
fun main() {
    val body = document.body ?: return
    ComposeViewport(body) {
        App(
            onNavHostReady = { navController ->
                navController.bindToBrowserNavigation() { entry ->
                    val route = entry.destination.route.orEmpty()
                    when {
                        // シリアル記述子を使用してルートを特定する
                        route.startsWith(StartScreen.serializer().descriptor.serialName) -> {
                            // 対応するURLフラグメントを "#org.example.app.StartScreen" の代わりに
                            // "#start" に設定する
                            //
                            // フロントエンドでの処理を維持するために、この文字列は
                            // 常に `#` 文字で始まる必要があります
                            "#start"
                        }
                        route.startsWith(Id.serializer().descriptor.serialName) -> {
                            // ルート引数にアクセスする
                            val args = entry.toRoute<Id>()

                            // 対応するURLフラグメントを "#org.example.app.ID%2F222" の代わりに
                            // "#find_id_222" に設定する
                            "#find_id_${args.id}"
                        }
                        route.startsWith(Patient.serializer().descriptor.serialName) -> {
                            val args = entry.toRoute<Patient>()
                            // 対応するURLフラグメントを "#org.company.app.Patient%2FJane%2520Smith-Baker%2F33" 
                            // の代わりに "#patient_Jane%20Smith-Baker_33" に設定する
                            "#patient_${args.name}_${args.age}"
                        }
                        // 他のすべてのルートにはURLフラグメントを設定しない
                        else -> ""
                    }
                }
            }
        )
    }
}

データをURLフラグメント内に保持するために、ルートに対応するすべての文字列が # 文字で始まっていることを確認してください。 そうしないと、ユーザーがURLをコピーして貼り付けたときに、ブラウザはアプリに制御を渡す代わりに、誤ったエンドポイントにアクセスしようとします。

URLにカスタムフォーマットを適用している場合は、手動で入力されたURLを目的地のルートに一致させるための逆変換処理を追加する必要があります。 マッチングを行うコードは、navController.bindToBrowserNavigation()呼び出しがブラウザの場所をナビゲーショングラフにバインドする前に実行する必要があります。

Kotlin
kotlin