Jetpack Navigation Compose 2026年版:型安全ナビゲーションと面接対策

Navigation Compose 2.10の型安全APIを詳細解説。Kotlin Serializationによるルート定義、ネストグラフ、ディープリンク、面接頻出質問まで網羅。

Jetpack Navigation Compose type-safe navigation diagram

Jetpack Navigation Compose 2.10は、Kotlin Serializationを活用した型安全なナビゲーションをAndroid開発に導入しています。このアプローチにより、引数の不一致によるランタイムクラッシュが排除され、大規模コードベースでのリファクタリングが大幅に簡素化されます。

Navigation 2.10 安定版

Navigation Compose 2.10.0は2026年8月にリリースされ、最小SDK 24が必要です。型安全ルートは文字列ベースの宛先ではなく@Serializableデータクラスを使用し、ナビゲーションエラーをコンパイル時に検出します。

型安全ナビゲーションAPIにはKotlin Serializationが必要です。モジュールレベルのbuild.gradle.ktsに以下の依存関係を追加します:

build.gradle.ktskotlin
plugins {
    id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0"
}

dependencies {
    implementation("androidx.navigation:navigation-compose:2.10.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}

宛先はシリアライズ可能な型として定義します。引数なしの画面にはobjectを、パラメータを受け取る画面にはdata classを使用します:

Routes.ktkotlin
import kotlinx.serialization.Serializable

@Serializable
object Home

@Serializable
object ProductList

@Serializable
data class ProductDetail(val productId: String)

@Serializable
data class Checkout(val cartId: String, val promoCode: String? = null)

コンパイラはすべてのルートクラスに@Serializableアノテーションが付与されていることを強制します。Navigation 2.9以降では、アノテーションの欠落はリントエラーを引き起こします。

シリアライズ可能ルートによるNavHostの構築

NavHostコンポーザブルはシリアライズ可能な型を直接受け入れます。各composable<T>ブロックは特定の宛先型を処理します:

AppNavigation.ktkotlin
@Composable
fun AppNavigation(
    navController: NavHostController = rememberNavController()
) {
    NavHost(
        navController = navController,
        startDestination = Home
    ) {
        composable<Home> {
            HomeScreen(
                onNavigateToProducts = { navController.navigate(ProductList) }
            )
        }

        composable<ProductList> {
            ProductListScreen(
                onProductClick = { productId ->
                    navController.navigate(ProductDetail(productId))
                }
            )
        }

        composable<ProductDetail> { backStackEntry ->
            // バックスタックエントリから型付きルートを抽出
            val route: ProductDetail = backStackEntry.toRoute()
            ProductDetailScreen(
                productId = route.productId,
                onCheckout = { cartId ->
                    navController.navigate(Checkout(cartId))
                }
            )
        }

        composable<Checkout> { backStackEntry ->
            val route: Checkout = backStackEntry.toRoute()
            CheckoutScreen(
                cartId = route.cartId,
                promoCode = route.promoCode
            )
        }
    }
}

toRoute<T>()拡張関数は、宛先の完全に型付けされたインスタンスを返します。データクラスのプロパティ名を変更すると、IDEリファクタリングを通じてすべての使用箇所が自動的に更新されます。

機能モジュール向けネストナビゲーショングラフ

大規模アプリケーションではネストナビゲーショングラフが有効です。各機能モジュールは独自のグラフを定義し、ルートグラフがそれらを構成します:

AuthNavigation.ktkotlin
@Serializable
object AuthGraph  // グラフ識別子

@Serializable
object Login

@Serializable
object Register

@Serializable
data class PasswordReset(val email: String)

fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
    navigation<AuthGraph>(startDestination = Login) {
        composable<Login> {
            LoginScreen(
                onRegisterClick = { navController.navigate(Register) },
                onForgotPassword = { email ->
                    navController.navigate(PasswordReset(email))
                },
                onLoginSuccess = {
                    navController.navigate(Home) {
                        popUpTo<AuthGraph> { inclusive = true }
                    }
                }
            )
        }

        composable<Register> {
            RegisterScreen(
                onRegistrationComplete = {
                    navController.navigate(Home) {
                        popUpTo<AuthGraph> { inclusive = true }
                    }
                }
            )
        }

        composable<PasswordReset> { backStackEntry ->
            val route: PasswordReset = backStackEntry.toRoute()
            PasswordResetScreen(email = route.email)
        }
    }
}

popUpTo<AuthGraph>inclusive = trueを組み合わせることで、ログイン成功後に認証フロー全体をバックスタックからクリアし、ユーザーがログイン画面に戻ることを防止します。

Androidの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

ルート引数としてのValue Class

Navigation 2.9.0-alpha03では、ルートにおけるvalue classのサポートが追加されました。このパターンはIDの型安全性を強制し、同じプリミティブ型のパラメータを混同することを防ぎます:

Domain.ktkotlin
@JvmInline
@Serializable
value class ProductId(val value: String)

@JvmInline
@Serializable
value class UserId(val value: String)

@Serializable
data class ProductDetail(val productId: ProductId)

@Serializable
data class UserProfile(val userId: UserId)

これにより、コンパイラはProductIdが期待される場所にUserIdを渡すことを防止します。これは文字列ベースのナビゲーションでよく発生するバグです。

型安全ルートによるディープリンク

ディープリンクは外部URIをシリアライズ可能なルートにマッピングします。パターンはcomposableブロックで定義します:

AppNavigation.ktkotlin
composable<ProductDetail>(
    deepLinks = listOf(
        navDeepLink {
            uriPattern = "https://example.com/products/{productId}"
        }
    )
) { backStackEntry ->
    val route: ProductDetail = backStackEntry.toRoute()
    ProductDetailScreen(productId = route.productId)
}

{productId}プレースホルダーはProductDetailデータクラスのproductIdプロパティに直接マッピングされます。Navigationは引数を自動的に抽出してデシリアライズします。

AndroidManifest.xmlにディープリンクを登録します:

xml
<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="example.com"
            android:pathPrefix="/products" />
    </intent-filter>
</activity>

Android 14以降では、ユーザーがコミット前に前の画面をプレビューできる予測バックジェスチャーがサポートされています。Navigation Compose 2.10はSeekableTransitionStateを通じてこれと統合します:

AppNavigation.ktkotlin
NavHost(
    navController = navController,
    startDestination = Home,
    enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start) },
    exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start) },
    popEnterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End) },
    popExitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End) }
) {
    // destinations
}

予測バックジェスチャーは、ユーザーがスワイプすると前の宛先をリアルタイムで表示します。閾値に達する前にジェスチャーを離すと、ナビゲーションがキャンセルされ現在の画面に戻ります。

シニアAndroid面接では、ナビゲーションアーキテクチャの決定についてしばしば深掘りされます。以下は経験豊富な候補者を見分ける質問です。

なぜ文字列ベースルートより型安全ナビゲーションを使うのですか?

"product/{productId}"のような文字列ベースルートは、引数のスペルミスや型の誤りでランタイムクラッシュを引き起こします。型安全ルートはこれらのエラーをコンパイル時に検出します。ルートパラメータのリファクタリングはIDEツールを通じてすべての使用箇所を更新しますが、文字列ベースルートはコードベース全体で手動の検索・置換が必要です。

大きなオブジェクトを渡さずに宛先間でデータを共有するにはどうすればよいですか?

大きなルート引数を避ける

ルートは保存インスタンス状態にシリアライズされます。商品カタログのような大きなオブジェクトを渡すとTransactionTooLargeExceptionのリスクがあります。IDを渡してリポジトリまたは共有ViewModelからデータを読み込む方式を採用してください。

データ共有のパターンにはスコープ付きViewModelを使用します:

ProductViewModel.ktkotlin
@HiltViewModel
class ProductViewModel @Inject constructor(
    private val repository: ProductRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    // SavedStateHandleから型付きルートを抽出
    private val productId: String = savedStateHandle.toRoute<ProductDetail>().productId

    val product = repository.getProduct(productId)
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
}

Composeでナビゲーションをテストするにはどうすればよいですか?

ナビゲーションテストは、ユーザーアクションが正しい宛先変更をトリガーすることを検証します:

NavigationTest.ktkotlin
class NavigationTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun clickingProduct_navigatesToDetail() {
        val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
        navController.navigatorProvider.addNavigator(ComposeNavigator())

        composeTestRule.setContent {
            AppNavigation(navController = navController)
        }

        // 商品リストに遷移
        composeTestRule.onNodeWithText("View Products").performClick()

        // 最初の商品をクリック
        composeTestRule.onNodeWithTag("product_item_0").performClick()

        // 現在の宛先を検証
        val currentRoute = navController.currentBackStackEntry?.toRoute<ProductDetail>()
        assertThat(currentRoute).isNotNull()
    }
}

Navigation 2.9.0-alpha01でこの動作が変更されました。ホストするActivityまたはFragmentが破棄された後にNavControllerを使用すると、IllegalStateExceptionがスローされるようになりました。以前のバージョンでは黙って失敗するか、未定義の動作を生じていました。この変更により、ライフサイクルのバグが開発の早い段階で表面化します。

認証状態に基づいた条件付きナビゲーションをどのように処理しますか?

一般的なパターンは認証状態を監視し、それに応じてリダイレクトします:

MainScreen.ktkotlin
@Composable
fun MainScreen(
    authState: AuthState,
    navController: NavHostController = rememberNavController()
) {
    LaunchedEffect(authState) {
        when (authState) {
            is AuthState.Authenticated -> {
                navController.navigate(Home) {
                    popUpTo(0) { inclusive = true }
                }
            }
            is AuthState.Unauthenticated -> {
                navController.navigate(AuthGraph) {
                    popUpTo(0) { inclusive = true }
                }
            }
            else -> { /* loading, do nothing */ }
        }
    }

    NavHost(
        navController = navController,
        startDestination = if (authState is AuthState.Authenticated) Home else AuthGraph
    ) {
        authNavGraph(navController)
        composable<Home> { HomeScreen() }
    }
}

LaunchedEffectは認証状態の変更に反応し、popUpTo(0) { inclusive = true }でバックスタック全体をクリアして、誤ったフローへの戻りナビゲーションを防止します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

文字列ルートから型安全ナビゲーションへの移行

既存のプロジェクトは段階的に移行できます。文字列ルートと型安全ルートは同じNavHost内で共存できます:

AppNavigation.ktkotlin
NavHost(
    navController = navController,
    startDestination = "home"  // レガシー文字列ルート
) {
    // レガシー文字列ベースの宛先
    composable("home") {
        HomeScreen(onNavigateToProduct = { id ->
            navController.navigate(ProductDetail(id))  // 新しい型安全ナビゲーション
        })
    }

    // 新しい型安全な宛先
    composable<ProductDetail> { backStackEntry ->
        val route: ProductDetail = backStackEntry.toRoute()
        ProductDetailScreen(productId = route.productId)
    }
}

このアプローチにより、ナビゲーショングラフ全体を書き換えることなく、一度に1画面ずつ移行できます。

2026年版Jetpack Navigation Composeの重要ポイント

  • Navigation 2.10.0は最小SDK 24の現行安定版です
  • 型安全ルートは文字列宛先の代わりに@Serializableデータクラスを使用します
  • Value classはIDに追加の型安全性を提供し、パラメータの混同を防止します
  • ネストナビゲーショングラフは独立したバックスタックで機能モジュールを整理します
  • ディープリンクはURIパターンをルートプロパティに直接マッピングします
  • 予測バックジェスチャーは標準トランジションと自動的に統合します
  • ルート引数は軽量に保つべきです:IDを渡し、リポジトリからデータを読み込みます
  • 文字列ルートからの移行は混合ナビゲーショングラフで段階的に行えます
  • Android面接対策でこれらのパターンを深く学べます
  • 関連するUIコンセプトについてはJetpack Compose面接質問も参照してください
今日のチャレンジ

Android のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年9月1日 更新

タグ

#android
#jetpack-compose
#navigation
#kotlin
#type-safe

共有

関連記事