jpskill.com
🛠️ 開発・MCP コミュニティ

navigation-compose

Jetpack Compose Navigationのパターンを利用し、型安全な経路設定、NavHostの構築、引数の受け渡し、ディープリンク、ネストされたナビゲーショングラフ、ボトムナビゲーションなどを効率的に実装するSkill。

📜 元の英語説明(参考)

Jetpack Compose Navigation patterns - type-safe routes, NavHost setup, argument passing, deep links, nested navigation graphs, and bottom navigation.

🇯🇵 日本人クリエイター向け解説

一言でいうと

Jetpack Compose Navigationのパターンを利用し、型安全な経路設定、NavHostの構築、引数の受け渡し、ディープリンク、ネストされたナビゲーショングラフ、ボトムナビゲーションなどを効率的に実装するSkill。

※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o navigation-compose.zip https://jpskill.com/download/16435.zip && unzip -o navigation-compose.zip && rm navigation-compose.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/16435.zip -OutFile "$d\navigation-compose.zip"; Expand-Archive "$d\navigation-compose.zip" -DestinationPath $d -Force; ri "$d\navigation-compose.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して navigation-compose.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → navigation-compose フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

Jetpack Compose Navigation Patterns

依存関係

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

Sealed Interface を用いた型安全なルート

@Serializable
sealed interface Route {
    @Serializable
    data object Home : Route

    @Serializable
    data object Settings : Route

    @Serializable
    data class UserProfile(val userId: String) : Route

    @Serializable
    data class PostDetail(val postId: Long, val showComments: Boolean = false) : Route
}

// ネストされたグラフ用
@Serializable
sealed interface AuthGraph {
    @Serializable
    data object Login : AuthGraph

    @Serializable
    data object Register : AuthGraph

    @Serializable
    data object ForgotPassword : AuthGraph
}

NavHost の設定

@Composable
fun AppNavHost(
    navController: NavHostController = rememberNavController(),
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Route.Home,
        modifier = modifier
    ) {
        composable<Route.Home> {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    navController.navigate(Route.UserProfile(userId))
                },
                onNavigateToSettings = {
                    navController.navigate(Route.Settings)
                }
            )
        }

        composable<Route.Settings> {
            SettingsScreen(onBack = { navController.popBackStack() })
        }

        composable<Route.UserProfile> { backStackEntry ->
            val route = backStackEntry.toRoute<Route.UserProfile>()
            UserProfileScreen(userId = route.userId)
        }

        composable<Route.PostDetail> { backStackEntry ->
            val route = backStackEntry.toRoute<Route.PostDetail>()
            PostDetailScreen(
                postId = route.postId,
                showComments = route.showComments
            )
        }
    }
}

レガシーな navArgument を用いた引数渡し

シリアライズできないルートには、従来の方法を使用します。

composable(
    route = "post/{postId}?showComments={showComments}",
    arguments = listOf(
        navArgument("postId") { type = NavType.LongType },
        navArgument("showComments") {
            type = NavType.BoolType
            defaultValue = false
        }
    )
) { backStackEntry ->
    val postId = backStackEntry.arguments?.getLong("postId") ?: return@composable
    val showComments = backStackEntry.arguments?.getBoolean("showComments") ?: false
    PostDetailScreen(postId = postId, showComments = showComments)
}

// ナビゲート
navController.navigate("post/$postId?showComments=true")

結果を伴うナビゲーション (SavedStateHandle)

// 画面 A: ナビゲートして結果をリッスンする
@Composable
fun ScreenA(navController: NavHostController) {
    val result = navController.currentBackStackEntry
        ?.savedStateHandle
        ?.getStateFlow<String?>("selected_item", null)
        ?.collectAsState()

    LaunchedEffect(result?.value) {
        result?.value?.let { item ->
            // 結果を処理する
        }
    }

    Button(onClick = { navController.navigate(Route.ItemPicker) }) {
        Text("Pick Item")
    }
}

// 画面 B: 結果を設定して戻る
@Composable
fun ItemPickerScreen(navController: NavHostController) {
    Button(onClick = {
        navController.previousBackStackEntry
            ?.savedStateHandle
            ?.set("selected_item", "chosen_value")
        navController.popBackStack()
    }) {
        Text("Select This")
    }
}

Deep Link の登録

composable<Route.PostDetail>(
    deepLinks = listOf(
        navDeepLink {
            uriPattern = "https://example.com/posts/{postId}"
        },
        navDeepLink {
            uriPattern = "myapp://posts/{postId}"
        }
    )
) { backStackEntry ->
    val route = backStackEntry.toRoute<Route.PostDetail>()
    PostDetailScreen(postId = route.postId)
}

AndroidManifest.xml の intent filter:

<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" />
        <data android:scheme="myapp" />
    </intent-filter>
</activity>

ネストされたナビゲーショングラフ

fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
    navigation<AuthGraph.Login>(startDestination = AuthGraph.Login) {
        composable<AuthGraph.Login> {
            LoginScreen(
                onLoginSuccess = {
                    navController.navigate(Route.Home) {
                        popUpTo(AuthGraph.Login) { inclusive = true }
                    }
                },
                onNavigateToRegister = {
                    navController.navigate(AuthGraph.Register)
                }
            )
        }
        composable<AuthGraph.Register> {
            RegisterScreen(onBack = { navController.popBackStack() })
        }
        composable<AuthGraph.ForgotPassword> {
            ForgotPasswordScreen(onBack = { navController.popBackStack() })
        }
    }
}

// メインの NavHost 内
NavHost(navController = navController, startDestination = AuthGraph.Login) {
    authNavGraph(navController)
    composable<Route.Home> { HomeScreen() }
}

ボトムナビゲーション

@Serializable
sealed interface BottomTab {
    @Serializable data object Feed : BottomTab
    @Serializable data object Search : BottomTab
    @Serializable data object Profile : BottomTab
}

data class BottomNavItem(
    val route: BottomTab,
    val label: String,
    val icon: ImageVector
)

val bottomNavItems = listOf(
    BottomNavItem(BottomTab.Feed, "Feed", Icons.Default.Home),
    BottomNa
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

Jetpack Compose Navigation Patterns

Dependencies

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

Type-Safe Routes with Sealed Interface

@Serializable
sealed interface Route {
    @Serializable
    data object Home : Route

    @Serializable
    data object Settings : Route

    @Serializable
    data class UserProfile(val userId: String) : Route

    @Serializable
    data class PostDetail(val postId: Long, val showComments: Boolean = false) : Route
}

// For nested graphs
@Serializable
sealed interface AuthGraph {
    @Serializable
    data object Login : AuthGraph

    @Serializable
    data object Register : AuthGraph

    @Serializable
    data object ForgotPassword : AuthGraph
}

NavHost Configuration

@Composable
fun AppNavHost(
    navController: NavHostController = rememberNavController(),
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Route.Home,
        modifier = modifier
    ) {
        composable<Route.Home> {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    navController.navigate(Route.UserProfile(userId))
                },
                onNavigateToSettings = {
                    navController.navigate(Route.Settings)
                }
            )
        }

        composable<Route.Settings> {
            SettingsScreen(onBack = { navController.popBackStack() })
        }

        composable<Route.UserProfile> { backStackEntry ->
            val route = backStackEntry.toRoute<Route.UserProfile>()
            UserProfileScreen(userId = route.userId)
        }

        composable<Route.PostDetail> { backStackEntry ->
            val route = backStackEntry.toRoute<Route.PostDetail>()
            PostDetailScreen(
                postId = route.postId,
                showComments = route.showComments
            )
        }
    }
}

Argument Passing with Legacy navArgument

For non-serializable routes, use the classic approach:

composable(
    route = "post/{postId}?showComments={showComments}",
    arguments = listOf(
        navArgument("postId") { type = NavType.LongType },
        navArgument("showComments") {
            type = NavType.BoolType
            defaultValue = false
        }
    )
) { backStackEntry ->
    val postId = backStackEntry.arguments?.getLong("postId") ?: return@composable
    val showComments = backStackEntry.arguments?.getBoolean("showComments") ?: false
    PostDetailScreen(postId = postId, showComments = showComments)
}

// Navigate
navController.navigate("post/$postId?showComments=true")

Navigation with Results (SavedStateHandle)

// Screen A: Navigate and listen for result
@Composable
fun ScreenA(navController: NavHostController) {
    val result = navController.currentBackStackEntry
        ?.savedStateHandle
        ?.getStateFlow<String?>("selected_item", null)
        ?.collectAsState()

    LaunchedEffect(result?.value) {
        result?.value?.let { item ->
            // Handle the result
        }
    }

    Button(onClick = { navController.navigate(Route.ItemPicker) }) {
        Text("Pick Item")
    }
}

// Screen B: Set result and go back
@Composable
fun ItemPickerScreen(navController: NavHostController) {
    Button(onClick = {
        navController.previousBackStackEntry
            ?.savedStateHandle
            ?.set("selected_item", "chosen_value")
        navController.popBackStack()
    }) {
        Text("Select This")
    }
}

Deep Link Registration

composable<Route.PostDetail>(
    deepLinks = listOf(
        navDeepLink {
            uriPattern = "https://example.com/posts/{postId}"
        },
        navDeepLink {
            uriPattern = "myapp://posts/{postId}"
        }
    )
) { backStackEntry ->
    val route = backStackEntry.toRoute<Route.PostDetail>()
    PostDetailScreen(postId = route.postId)
}

AndroidManifest.xml intent filter:

<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" />
        <data android:scheme="myapp" />
    </intent-filter>
</activity>

Nested Navigation Graphs

fun NavGraphBuilder.authNavGraph(navController: NavHostController) {
    navigation<AuthGraph.Login>(startDestination = AuthGraph.Login) {
        composable<AuthGraph.Login> {
            LoginScreen(
                onLoginSuccess = {
                    navController.navigate(Route.Home) {
                        popUpTo(AuthGraph.Login) { inclusive = true }
                    }
                },
                onNavigateToRegister = {
                    navController.navigate(AuthGraph.Register)
                }
            )
        }
        composable<AuthGraph.Register> {
            RegisterScreen(onBack = { navController.popBackStack() })
        }
        composable<AuthGraph.ForgotPassword> {
            ForgotPasswordScreen(onBack = { navController.popBackStack() })
        }
    }
}

// In the main NavHost
NavHost(navController = navController, startDestination = AuthGraph.Login) {
    authNavGraph(navController)
    composable<Route.Home> { HomeScreen() }
}

Bottom Navigation

@Serializable
sealed interface BottomTab {
    @Serializable data object Feed : BottomTab
    @Serializable data object Search : BottomTab
    @Serializable data object Profile : BottomTab
}

data class BottomNavItem(
    val route: BottomTab,
    val label: String,
    val icon: ImageVector
)

val bottomNavItems = listOf(
    BottomNavItem(BottomTab.Feed, "Feed", Icons.Default.Home),
    BottomNavItem(BottomTab.Search, "Search", Icons.Default.Search),
    BottomNavItem(BottomTab.Profile, "Profile", Icons.Default.Person)
)

@Composable
fun MainScreen() {
    val navController = rememberNavController()
    val navBackStackEntry by navController.currentBackStackEntryAsState()

    Scaffold(
        bottomBar = {
            NavigationBar {
                bottomNavItems.forEach { item ->
                    val isSelected = navBackStackEntry?.destination?.hasRoute(
                        item.route::class
                    ) == true

                    NavigationBarItem(
                        selected = isSelected,
                        onClick = {
                            navController.navigate(item.route) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        },
                        icon = { Icon(item.icon, contentDescription = item.label) },
                        label = { Text(item.label) }
                    )
                }
            }
        }
    ) { padding ->
        NavHost(
            navController = navController,
            startDestination = BottomTab.Feed,
            modifier = Modifier.padding(padding)
        ) {
            composable<BottomTab.Feed> { FeedScreen() }
            composable<BottomTab.Search> { SearchScreen() }
            composable<BottomTab.Profile> { ProfileScreen() }
        }
    }
}

Animated Transitions

composable<Route.PostDetail>(
    enterTransition = {
        slideIntoContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Left,
            animationSpec = tween(300)
        )
    },
    exitTransition = {
        slideOutOfContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Left,
            animationSpec = tween(300)
        )
    },
    popEnterTransition = {
        slideIntoContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Right,
            animationSpec = tween(300)
        )
    },
    popExitTransition = {
        slideOutOfContainer(
            towards = AnimatedContentTransitionScope.SlideDirection.Right,
            animationSpec = tween(300)
        )
    }
) { backStackEntry ->
    val route = backStackEntry.toRoute<Route.PostDetail>()
    PostDetailScreen(postId = route.postId)
}

Navigation Testing

@Test
fun navigateToProfile_displaysUserProfile() {
    val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

    composeTestRule.setContent {
        navController.navigatorProvider.addNavigator(ComposeNavigator())
        AppNavHost(navController = navController)
    }

    composeTestRule.onNodeWithText("View Profile").performClick()

    val currentRoute = navController.currentBackStackEntry?.destination?.route
    assertTrue(currentRoute?.contains("UserProfile") == true)
}

Best Practices

  • Use type-safe routes with @Serializable data classes/objects over raw string routes.
  • Keep navigation logic out of composables; pass lambda callbacks (onNavigateTo) instead.
  • Use popUpTo with inclusive = true when navigating after login to clear the auth stack.
  • Use launchSingleTop = true for bottom tabs to prevent duplicate destinations.
  • Save and restore tab state with saveState = true and restoreState = true.
  • Scope ViewModels to navigation entries with hiltViewModel() or koinViewModel().
  • Test navigation by asserting on navController.currentBackStackEntry.