iOSエンジニアのためのAndroid Navigation入門
iOSでUINavigationControllerやNavigationStack1を使った画面遷移に慣れているエンジニアが、Android Jetpack Navigation2を学ぶときに「これはiOSでいう何?」と感じるポイントは多い。
この記事では、Android Navigationの主要概念をiOSの対応概念と対比しながら解説する。Navigation Compose3(Jetpack Compose4向け)を中心に扱う。
NavController — 画面遷移の中心
概要
NavController5はAndroid Navigationの中核で、画面遷移の実行とバックスタック6の管理を担う。iOSでいうUINavigationControllerに最も近い存在だ。
| Android | iOS | |
|---|---|---|
| クラス名 | NavController | UINavigationController |
| Compose版 | NavHostController | NavigationStack (SwiftUI) |
| 取得方法 | rememberNavController() | navigationController プロパティ |
| 前画面に戻る | popBackStack() | popViewController() |
| ルートまで戻る | popBackStack(route, inclusive) | popToRootViewController() |
取得方法
// Compose: NavControllerの作成と保持
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") { HomeScreen(navController) }
composable("detail/{id}") { DetailScreen(navController) }
}
}
iOSのUINavigationControllerはStoryboard7かコードで生成するが、AndroidではComposable関数内でrememberNavController()を呼ぶだけでよい。
💡 iOSのself.navigationControllerに相当するのがnavController。ただしAndroidでは明示的に引数で渡すか、LocalNavControllerを使ってCompositionLocal8経由で取得する。
Route — 画面遷移先の指定方法
文字列ベースのRoute(従来方式)
Android Navigationでは遷移先を文字列で指定する。iOSのSegue9識別子に近いが、URLパスのような書式を使う。
// 画面遷移(文字列Route)
navController.navigate("detail/42")
// ルート定義
composable("detail/{id}") { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
DetailScreen(id = id)
}
Type-safe Route(Navigation Compose 2.8+)
Navigation Compose 2.8以降では、Kotlin Serialization10を使った型安全なRouteが導入された。文字列のtypoによるクラッシュを防げる。
// Route定義(データクラス)
@Serializable
data class DetailRoute(val id: Int)
@Serializable
object HomeRoute
// NavHost定義
NavHost(
navController = navController,
startDestination = HomeRoute
) {
composable<HomeRoute> { HomeScreen(navController) }
composable<DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<DetailRoute>()
DetailScreen(id = route.id)
}
}
// 遷移
navController.navigate(DetailRoute(id = 42))
iOS対比
| Android | iOS | 備考 |
|---|---|---|
文字列Route "detail/42" | Segue identifier | 文字列ベース、typoに弱い |
Type-safe Route DetailRoute(id=42) | NavigationLink(value:) (SwiftUI) | 型安全 |
| Route定義 (composable) | Storyboard / Router / Coordinator11 | 遷移先の登録 |
📝 iOSのCoordinatorパターンはAndroidには直接的な対応物がないが、NavGraphのネストが似た役割を果たす。画面遷移のロジックをグラフ構造として宣言的に定義する点で共通している。
NavGraph / サブグラフ — 遷移グラフの構造化
NavGraphとは
NavGraph12はアプリ内の全画面遷移を定義するグラフ構造だ。どの画面からどの画面に遷移できるかを宣言的に記述する。iOSのStoryboard7に近い概念だが、コードで定義する点が異なる。
NavHost(
navController = navController,
startDestination = "home" // 最初に表示する画面
) {
composable("home") { HomeScreen(navController) }
composable("settings") { SettingsScreen(navController) }
composable("profile/{userId}") { ProfileScreen(navController) }
}
startDestination
startDestinationはNavGraphのエントリーポイント。iOSのStoryboardにおけるInitial View Controller13に相当する。
ネストされたサブグラフ
大規模アプリでは、機能ごとにサブグラフ(ネストされたNavGraph)を定義してモジュール化する。
NavHost(
navController = navController,
startDestination = "main"
) {
// メイン画面群
navigation(startDestination = "home", route = "main") {
composable("home") { HomeScreen(navController) }
composable("detail/{id}") { DetailScreen(navController) }
}
// 認証画面群(サブグラフ)
navigation(startDestination = "login", route = "auth") {
composable("login") { LoginScreen(navController) }
composable("register") { RegisterScreen(navController) }
}
}
| Android | iOS | 備考 |
|---|---|---|
| NavGraph | Storyboard | 画面遷移の全体定義 |
サブグラフ (navigation {}) | Storyboard Reference / 子Coordinator | 機能単位の分割 |
startDestination | Initial View Controller | エントリーポイント |
navOptions / popUpTo — バックスタック制御
popUpToでスタックを巻き戻す
popUpToは指定した画面までバックスタックを巻き戻す。iOSのpopToViewController(_:animated:)に相当する。
// "home"までスタックを巻き戻してから"settings"に遷移
navController.navigate("settings") {
popUpTo("home") {
inclusive = false // "home"自体は残す
}
}
inclusive = trueにすると、指定した画面自体もスタックから除去される。iOSのpopToRootViewController()に近い動作になる。
// ログイン後にauthグラフ全体をスタックから除去
navController.navigate("home") {
popUpTo("auth") {
inclusive = true // "auth"グラフ自体も除去
}
}
launchSingleTop
launchSingleTop = trueは、同じ画面がスタックの最上位にある場合に重複生成を防ぐ。iOSでは手動で実装する必要があるパターンだ。
navController.navigate("home") {
launchSingleTop = true // 同じ"home"がtopにあれば再利用
popUpTo("home") {
inclusive = true
saveState = true
}
restoreState = true
}
| Android | iOS | 備考 |
|---|---|---|
popUpTo(route) | popToViewController() | 指定画面まで戻る |
popUpTo(route) { inclusive = true } | popToRootViewController() + dismiss | ルートごと除去 |
launchSingleTop | 手動実装が必要 | 重複画面防止 |
saveState / restoreState | 該当なし | BottomNav切替時の状態保存 |
⚡ popUpToとinclusiveの組み合わせは最初は混乱するが、「ログイン→ホーム遷移でログイン画面を戻り先から消す」というユースケースで理解するのが一番分かりやすい。
画面間のデータ受け渡し
Route引数(型安全)
Type-safe Routeを使えば、画面間のデータ受け渡しはRouteクラスのプロパティとして自然に表現できる。
// Route定義にデータを含める
@Serializable
data class DetailRoute(val id: Int, val title: String)
// 遷移時にデータを渡す
navController.navigate(DetailRoute(id = 42, title = "Quest Name"))
// 遷移先で受け取る
composable<DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<DetailRoute>()
DetailScreen(id = route.id, title = route.title)
}
SavedStateHandle — 前の画面に結果を返す
iOSのdelegateパターンや@Binding14に相当するのがSavedStateHandle15だ。画面Bから画面Aに結果を返すときに使う。
// 画面A: 結果を待ち受ける
composable("screenA") { backStackEntry ->
val result = backStackEntry.savedStateHandle
.get<String>("result_key")
ScreenA(
result = result,
onNavigateToB = { navController.navigate("screenB") }
)
}
// 画面B: 結果を設定して戻る
composable("screenB") {
ScreenB(onConfirm = { value ->
navController.previousBackStackEntry
?.savedStateHandle
?.set("result_key", value)
navController.popBackStack()
})
}
| Android | iOS | 備考 |
|---|---|---|
| Route引数 | prepareForSegue / NavigationLink(value:) | 遷移先にデータを渡す |
SavedStateHandle | delegate / @Binding / onDismiss | 前画面に結果を返す |
ViewModel16共有 | @EnvironmentObject | 画面間で状態を共有 |
💡 iOSでは@Bindingやdelegateで前画面に結果を返すのが自然だが、AndroidではSavedStateHandleがナビゲーション結果の標準的な受け渡し方法。プロセス再生成後も値が復元される利点がある。
DeepLink対応
NavDeepLink
Android Navigationは、URLベースのDeepLink17をNavGraphに直接統合できる。
composable(
route = "detail/{id}",
deepLinks = listOf(
navDeepLink {
uriPattern = "https://example.com/detail/{id}"
}
)
) { backStackEntry ->
val id = backStackEntry.arguments?.getString("id")
DetailScreen(id = id)
}
アプリ外からのURLリンクが、NavGraph上の正しい画面に自動的にルーティングされる。バックスタックも適切に構築される(DeepLinkで直接Detail画面に飛んでも、戻るボタンでHome画面に戻れる)。
AndroidManifest.xmlへの登録
DeepLinkを有効にするには、AndroidManifest.xml18に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"
android:pathPattern="/detail/.*" />
</intent-filter>
</activity>
iOS対比
| Android | iOS | 備考 |
|---|---|---|
navDeepLink { uriPattern = ... } | onOpenURL (SwiftUI) / application(_:open:) | URLハンドリング |
| intent-filter (Manifest) | Associated Domains + apple-app-site-association19 | OSへの登録 |
| 自動バックスタック構築 | 手動実装が必要 | DeepLink時の戻り先 |
📝 iOSのUniversal Links20はサーバー側にapple-app-site-associationファイルが必要だが、AndroidのDeepLinkはManifestへの登録だけで動く(App Links21はサーバー検証が必要)。
まとめ — iOS ↔ Android 画面遷移対応表
| 概念 | Android | iOS (UIKit) | iOS (SwiftUI) |
|---|---|---|---|
| 遷移コントローラ | NavController | UINavigationController | NavigationStack |
| 遷移先の指定 | Route (文字列 or 型安全) | Segue / push | NavigationLink(value:) |
| 遷移グラフ | NavGraph | Storyboard | navigationDestination |
| バックスタック制御 | popUpTo | popToViewController | NavigationPath |
| データ受け渡し | Route引数 / SavedStateHandle | prepareForSegue / delegate | @Binding / onDismiss |
| DeepLink | NavDeepLink + Manifest | Universal Links + AASA | onOpenURL |
iOSとAndroidの画面遷移は設計思想が異なるが、解決しようとしている問題は同じだ。iOSでの経験をベースにしながら、AndroidではNavGraphという「宣言的な遷移定義」が中心にあることを意識すると、学習がスムーズになる。
Footnotes
-
Jetpack Compose — Androidの宣言的UIフレームワーク。iOSのSwiftUIに相当する。Kotlin DSLでUIを記述する。 ↩
-
バックスタック — 画面遷移の履歴をスタック(LIFO)構造で管理するもの。戻るボタンを押すとスタックの最上位が除去され、前の画面に戻る。 ↩
-
Storyboard — iOS開発におけるビジュアルな画面遷移エディタ。XML形式で画面のレイアウトと遷移(Segue)を定義する。 ↩ ↩2
-
CompositionLocal — Jetpack Composeの暗黙的データ伝搬メカニズム。SwiftUIの
@Environmentに相当する。 ↩ -
Segue — iOSのStoryboard上で定義する画面遷移。識別子(文字列)で遷移先を指定する。 ↩
-
Kotlin Serialization — Kotlin公式のシリアライゼーションライブラリ。データクラスをJSON等に変換する。Navigation Compose 2.8+ではRoute定義にも使われる。 ↩
-
Coordinator — iOS開発で画面遷移ロジックをViewControllerから分離するデザインパターン。ViewControllerの肥大化を防ぐ。 ↩
-
Initial View Controller — StoryboardのEntry Point。アプリ起動時に最初に表示されるViewControllerを示す矢印マーク。 ↩
-
@Binding — SwiftUIのプロパティラッパー。親ビューの状態を子ビューから読み書きできるようにする双方向バインディング。 ↩
-
SavedStateHandle — AndroidのViewModelと連携するキーバリューストア。画面間の結果受け渡しやプロセス再生成後の状態復元に使う。 ↩
-
ViewModel — Android Architecture Componentsのクラス。画面回転やConfiguration変更を生き延びるデータホルダー。iOSのObservableObjectに近い。 ↩
-
DeepLink — アプリ外のURL等から、アプリ内の特定画面に直接遷移する仕組み。 ↩
-
AndroidManifest.xml — Androidアプリの設定ファイル。パーミッション、Activity、intent-filter等をOSに宣言する。iOSのInfo.plistに相当。 ↩
-
apple-app-site-association — Universal LinksでiOSアプリとWebドメインを関連付けるためのJSONファイル。Webサーバーのルートに配置する。 ↩
-
Universal Links — iOSでHTTPS URLをアプリ内の特定画面に直接ルーティングする仕組み。サーバー側にapple-app-site-associationファイルが必要。 ↩
-
App Links — AndroidでHTTPS URLをアプリに関連付ける仕組み。サーバー側にDigital Asset Linksファイル(
.well-known/assetlinks.json)を配置してドメイン所有を検証する。 ↩