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本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
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
$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. 下の青いボタンを押して
navigation-compose.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
navigation-composeフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
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
@Serializabledata classes/objects over raw string routes. - Keep navigation logic out of composables; pass lambda callbacks (
onNavigateTo) instead. - Use
popUpTowithinclusive = truewhen navigating after login to clear the auth stack. - Use
launchSingleTop = truefor bottom tabs to prevent duplicate destinations. - Save and restore tab state with
saveState = trueandrestoreState = true. - Scope ViewModels to navigation entries with
hiltViewModel()orkoinViewModel(). - Test navigation by asserting on
navController.currentBackStackEntry.