NavControllerはNavigation Composeライブラリの中心的なコンポーネントであり、Androidアプリケーションのナビゲーションスタックとback stackの状態を管理します。NavControllerを通じて、画面間の遷移、前のページへの復帰、ルート間のデータ転送が実行されます。Android Developers (2025)によると、NavControllerは複数の画面を持つComposeアプリケーションに必須の要素です。コントローラはrememberNavController()で作成され、NavHostに渡され、コンポジションの任意の場所からnavigate()を呼び出すことができます。組み込みのSavedStateHandleサポートにより、再構成時にViewModelの状態が自動的に保存されます。
重要なポイント
NavControllerはNavigation Composeライブラリのクラスで、Composeアプリケーション用のナビゲーションコントローラを実装します。NavControllerはNavBackStackEntryスタックを管理し、各エントリにはルート、引数、画面状態が含まれます。コントローラは基本的なナビゲーション操作(遷移、復帰、置換、クリーンアップ)をサポートします。
Viewシステム(FragmentManagerやIntentを使用)とは異なり、NavControllerはComposeコンテキストでのみ動作します。Back stackはFragmentスタックではなくNavDestinationグラフとして保存されます。これによりFragmentの作成と破棄のオーバーヘッドがなくなり、テストが簡素化されます。NavControllerはTestNavHostControllerを介してモック化できます。
NavControllerはNavHostと密接に関連しています。NavHostはグラフから現在の画面をレンダリングするコンテナです。NavHostがないと、NavControllerはcomposable関数を表示できませんが、スタックを管理する機能は保持します。典型的なアーキテクチャでは、NavControllerはActivityまたはメインコンポーザブルのレベルで作成され、パラメータを介してコンポジションツリーの下位に渡されます。
Googleによると、NavControllerはいくつかのメジャーリリースを経てきました。バージョン2.8.0ではType-Safe Navigationが追加され、バージョン2.9.0ではpredictive back gesture(Android 14+)のサポートが追加されました。コントローラはMaterial3 ScaffoldおよびBottomNavigationと互換性があります。マルチモジュールプロジェクトでは、NavControllerはDI(Hilt/Koin)またはコンストラクタパラメータを介して渡されます。
NavControllerはcomposable関数rememberNavController()で作成されます。この関数は、現在のcomposableのライフサイクルにバインドされたNavHostController(NavControllerのサブクラス)のインスタンスを返します。コンポジションを離れると、コントローラはクリアされます。再構成時にコントローラを保持するには、rememberSaveableまたはViewModelを使用します。
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "main"
) {
composable("main") { MainScreen(navController) }
composable("details") { DetailsScreen(navController) }
}
}
NavControllerの設定には次のものがあります:NavHostController(メイン)、TestNavHostController(テスト)、ScopedNavController(ネストされたグラフ用の子)。BottomNavigationの場合、NavControllerはアプリ全体で単一である必要があります。各タブで新しいコントローラを作成するとスタックが失われます。ネストされた画面にコントローラを渡すには、可読性を維持するためにCompositionLocalProviderではなく関数パラメータを使用します。
ナビゲーションテストには、compose-test-ruleとともにTestNavHostControllerを使用します。このコントローラでは、初期ルートを設定し、navigate()が期待される遷移をトリガーしたことを確認できます。NavControllerのテストにエミュレータは不要で、Compose TestのSemanticsマッチャーで動作します。
navigate(route: String)メソッドはNavControllerの主要なナビゲーションメカニズムです。ルート文字列、オプションのNavOptions、Navigator.Extrasを受け入れます。NavOptionsは遷移動作を制御します:launchSingleTop(スタック内のルートを複製しない)、popUpTo(指定ルートまでスタックをクリア)、restoreState(以前の状態を復元)。
NavOptionsはビルダー構文(NavOptionsBuilder)で設定します。主なパラメータ:popUpTo(ルート+inclusive/saveState)、launchSingleTop(Boolean、true — 複製を作成しない)、restoreState(復帰時に状態を復元)。popUpToがないと、navigate()のたびにスタックにエントリが追加され、back stackが蓄積されてBackボタンの動作が不正になります。
navController.navigate("profile/42") {
popUpTo("main") { saveState = true }
launchSingleTop = true
restoreState = true
}
Navigator.Extrasを使用すると、ルートの一部ではない追加データ(アニメーション用の共有要素、Intentフラグ、Pac-Manバンドル)を渡せます。Extrasはめったに使用されず、主にAccompanist AnimationやカスタムNavigatorとの統合に使用されます。ほとんどのシナリオでは、ルート文字列とNavOptionsで十分です。
popBackStack()は前の画面に戻るためのメソッドです。引数なしではスタックの一番上のエントリを削除し、成功した場合はtrueを返します。スタックが空の場合、メソッドはfalseを返し、Activityが閉じます(super.onBackPressed()と同様)。
オーバーロード版のpopBackStack(route: String, inclusive: Boolean)は、指定されたルートまでのすべてのエントリを削除します。inclusive = trueの場合、指定されたルート自体も削除されます。メソッドはBooleanを返します。エントリが見つかって削除された場合はtrueです。inclusive版は、認証後や注文完了後の「ルート画面に戻る」シナリオに便利です。
| メソッド | 説明 | 例 |
|---|---|---|
| popBackStack() | 1つ前の画面に戻る | navController.popBackStack() |
| popBackStack(route, false) | routeまでクリア(routeは残る) | popBackStack(“home”, false) |
| popBackStack(route, true) | routeまでを含めてクリア | popBackStack(“home”, true) |
| navigate(route) { popUpTo(route) { inclusive = true } } | 完全クリアして遷移 | navigate(“login”) { popUpTo(0) { inclusive = true } } |
システムのBackボタン(ハードウェアバックボタン)を処理するには、ComposeのBackHandlerを使用します。BackHandlerはenabledとonBack(押下時に呼び出されるコールバック)を受け入れます。Android 14+では、NavControllerバージョン2.9.0から統合されたPredictiveBackGestureが使用されます。Predictive backは復帰のプレビューアニメーションを追加します。
SavedStateHandleは、ナビゲーションおよび再構成中にViewModelの状態を保持するためのメカニズムです。NavControllerは各NavBackStackEntryにSavedStateHandleを自動的に提供します。SavedStateHandleを介して、ViewModelは画面状態を保存し、復帰時に復元します(restoreState = true)。
Navigation Composeでは、SavedStateHandleはViewModelと一緒に使用されます。ViewModelはbackStackEntryから渡されるSavedStateHandleを介して初期化されます。別の画面に遷移して戻る場合(restoreStateあり)、ViewModelは新しく作成されるのではなく、保存された状態を受け取ります。これはデータ入力、フィルター、スクロールを伴う画面にとって重要です。
class ProfileViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId: String = savedStateHandle.get<String>("userId") ?: ""
var searchQuery by savedStateHandle.getStateFlow("search", "")
.collectAsState()
}
SavedStateHandleはプリミティブ型、String、Bundle、Parcelableをサポートします。複雑なオブジェクトの場合は、IDのみを保存し、完全なデータはリポジトリからロードします。SavedStateHandleの制限は約1MBで、これを超えるとTransactionTooLargeExceptionが発生します。大量のデータの場合は、handleに保存する代わりにRoomまたはDataStoreを使用します。
重要:SavedStateHandleはNavOptionsでrestoreState = trueが使用されている場合にのみ状態を保持します。restoreStateが指定されていない場合、復帰時にViewModelはデフォルト値で新しく作成されます。restoreStateを使用したBottomNavigationの切り替えでは、NavControllerは各タブの状態を保持し、再選択時に復元します。
currentBackStackEntryAsState()はState<NavBackStackEntry?>を返す関数で、現在のルートが変更されるたびに更新されます。これはナビゲーションとUIを同期する主要なメカニズムです。BottomNavigationはアクティブな項目を強調表示し、Toolbarはタイトルを更新し、Drawerは遷移時に閉じます。
この関数はsnapshotFlowとcollectAsStateを介して動作します。back stackが変更されると、Composeはサブスクライブされた要素を再コンポーズします。重要:currentBackStackEntryAsState()は遷移アニメーションが完了した後にのみ更新されます。即時更新が必要な場合は、currentDestinationを使用します。これはnavigate()と同期的に変更されますが、状態はサポートしません。
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Text(
text = when (currentRoute) {
"home" -> "Home"
"profile" -> "Profile"
else -> ""
}
)
現在のルートの引数にアクセスするには、navBackStackEntry?.argumentsを使用します。これはBottomNavigationで便利です。selectedItemはcurrentRouteに基づいて計算されます。ナビゲーションデバッグには、各遷移をログに記録するNavController.addOnDestinationChangedListener()を使用します。本番環境では、多数のcomposable内でのサブスクリプションを避け、ViewModelに単一のソースを作成してStateをUIに渡します。
よくある質問
技術的には可能ですが、推奨されません。単一のNavControllerが一貫したback stackを保証し、デバッグを簡素化します。複数のコントローラは、個別のナビゲーションを持つネストされたグラフ(独自のスタックを持つmodal bottom sheetなど)にのみ正当化されます。
コンストラクタまたはDIを介してNavControllerをViewModelに渡します。ただし、NavController自体ではなくコールバック関数(onNavigate、onBack)のみを渡す方がテストが簡素化されるため推奨されます。イベントには、ViewModelでChannel<NavEvent>を使用し、UIで収集します。
問題はライフサイクルにあります。NavControllerがまだ初期化されていない場合(NavHostが構築されていない)、navigate()は無視されます。データロード後にナビゲーションを呼び出すには、任意のライフサイクルを持つコルーチン内ではなく、LaunchedEffectを使用します。
navController.navigate(“target”) { popUpTo(0) { inclusive = true } }を呼び出します。popUpTo(0)パラメータはスタックを完全にクリアし、inclusive = trueは開始エントリも削除します。launchSingleTop = trueフラグはルートの重複を防ぎます。
NavHostControllerはNavControllerのサブクラスで、NavHost用の追加メソッド(setOnBackStackChangedListenerなど)があります。NavControllerはベースクラスで、NavHostの外部でプログラムによるスタック管理に使用できます。ほとんどの場合、NavHostControllerが使用されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。