Navigation コンポーネントは Jetpack Compose アプリをサポートしています。Navigation コンポーネントのインフラストラクチャと機能を活用しながら、コンポーザブル間を移動することができます。
設定
Compose をサポートするには、アプリ モジュールの build.gradle
ファイルで次の依存関係を使用します。
Groovy
dependencies { def nav_version = "2.5.3" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { def nav_version = "2.5.3" implementation("androidx.navigation:navigation-compose:$nav_version") }
はじめに
NavController
は Navigation コンポーネントの中心的な API です。この API はステートフルであり、アプリ内の画面を構成するコンポーザブルのバックスタックと各画面の状態を追跡します。
NavController
を作成するには、コンポーザブルで rememberNavController()
メソッドを使用します。
val navController = rememberNavController()
NavController
は、これを参照する必要があるすべてのコンポーザブルがアクセスできるコンポーザブル階層内の場所に作成する必要があります。これは状態ホイスティングの原則に従っており、NavController
とその currentBackStackEntryAsState()
を介して提供される状態を、画面の外部でコンポーザブルを更新する際の信頼できる情報として使用できるようになります。この機能の例については、下部のナビゲーション バーとの統合をご覧ください。
NavHost の作成
各 NavController
は 1 つの NavHost
コンポーザブルに関連付ける必要があります。NavHost
は NavController
にナビゲーション グラフを関連付けます。ナビゲーション グラフではコンポーザブルのデスティネーション(目的地)が指定されており、それらのデスティネーション間を移動できるようになります。コンポーザブル間を移動すると、NavHost
のコンテンツは自動的に再コンポーズされます。ナビゲーション グラフ内の各コンポーザブルのデスティネーションには「ルート」が関連付けられています。
NavHost
を作成するには、rememberNavController()
を使って以前に作成された NavController
と、グラフの開始デスティネーションのルートが必要です。NavHost
の作成では、Navigation Kotlin DSL のラムダ構文を使用して、ナビゲーション グラフを作成します。composable()
メソッドを使用して、ナビゲーション構造に追加できます。このメソッドでは、デスティネーションにリンクするルートとコンポーザブルを指定する必要があります。
NavHost(navController = navController, startDestination = "profile") {
composable("profile") { Profile(/*...*/) }
composable("friendslist") { FriendsList(/*...*/) }
/*...*/
}
コンポーザブルに移動する
ナビゲーション グラフ内のコンポーザブルのデスティネーションに移動するには、navigate
メソッドを使用する必要があります。navigate
は、デスティネーションのルートを表す単一の String
パラメータを取ります。ナビゲーション グラフ内でコンポーザブルから移動するには、navigate
を呼び出します。
navController.navigate("friendslist")
デフォルトでは、navigate
は新しいデスティネーションをバックスタックに追加します。navigate
の動作を変更するには、追加のナビゲーション オプションを navigate()
呼び出しにアタッチします。
// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home")
}
// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
popUpTo("home") { inclusive = true }
}
// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
launchSingleTop = true
}
その他のユースケースについては、popUpTo のガイドをご覧ください。
他のコンポーズ可能な関数によってトリガーされた呼び出しを移動する
NavController
の navigate
関数は、NavController
の内部状態を変更します。信頼できる唯一の情報源の原則に可能な限り従うには、NavController
インスタンスをホイスティングするコンポーズ可能な関数または状態ホルダー、および NavController
をパラメータとして受け取るコンポーズ可能な関数のみがナビゲーション呼び出しを行うようにする必要があります。UI 階層の下位にある他のコンポーズ可能な関数からトリガーされたナビゲーション イベントは、関数を適切に使用する呼び出し元に公開する必要があります。
次の例は、コンポーズ可能な関数 MyAppNavHost
を、NavController
インスタンスの信頼できる唯一の情報源として示しています。ProfileScreen
はイベントを、ユーザーがボタンをタップしたときに呼び出される関数として公開します。MyAppNavHost
は、アプリ内の異なる画面へのナビゲーションを所有し、ProfileScreen
を呼び出すときに適切なデスティネーションに対してナビゲーション呼び出しを行います。
@Composable
fun MyAppNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = "profile"
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable("profile") {
ProfileScreen(
onNavigateToFriends = { navController.navigate("friendsList") },
/*...*/
)
}
composable("friendslist") { FriendsListScreen(/*...*/) }
}
}
@Composable
fun ProfileScreen(
onNavigateToFriends: () -> Unit,
/*...*/
) {
/*...*/
Button(onClick = onNavigateToFriends) {
Text(text = "See friends list")
}
}
再コンポーズのたびに navigate()
が呼び出されるのを避けるため、navigate()
は、コンポーザブル自体の一部としてではなく、常にコールバックの一部として呼び出してください。
おすすめの方法
アプリ内で特定のロジックを処理する方法を知っている呼び出し元に、コンポーズ可能な関数からのイベントを公開することは、状態をホイスティングする場合の Compose でのおすすめの方法です。
イベントを個別のラムダ パラメータとして公開することで、関数シグネチャがオーバーロードされる可能性がありますが、コンポーズ可能な関数で行う処理が最大限に可視化されます。その関数の機能が一目でわかります。
関数宣言でパラメータの数を減らせるその他の方法は、最初は書くのが楽かもしれませんが、長期的には欠点がいくつかあります。たとえば、すべてのイベントを 1 か所に一元化する ProfileScreenEvents
のようなラッパークラスを作成する方法です。これを行うと、関数の定義の際にコンポーザブルが行う処理の可視性が低下し、さらに別のクラスやメソッドがプロジェクトに追加されます。そのため、コンポーズ可能な関数を呼び出すたびに、そのクラスのインスタンスを作成して覚えておくことが必要になります。また、そのラッパークラスをできる限り再利用する場合、このパターンでは、そのクラスのインスタンスを UI 階層の下位に渡すことになりがちです。これは、必要なものだけをコンポーザブルに渡すというおすすめの方法に反します。
引数を使用して移動する
Navigation Compose では、コンポーザブルのデスティネーション間で引数を渡すこともできます。これを行うには、基本のナビゲーション ライブラリを使用する場合にディープリンクに引数を追加する方法と同様に、引数のプレースホルダをルートに追加する必要があります。
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
デフォルトでは、すべての引数が文字列として解析されます。composable()
の arguments
パラメータは NamedNavArgument
のリストを受け取ります。navArgument
メソッドを使用して NamedNavArgument
をすばやく作成してから、その正確な type
を指定できます。
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
composable()
関数のラムダで使用可能な NavBackStackEntry
から引数を抽出する必要があります。
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
デスティネーションに引数を渡すには、navigate
を呼び出すときにルートに引数を追加する必要があります。
navController.navigate("profile/user1234")
サポートされる型の一覧については、デスティネーション間でデータを渡すをご覧ください。
移動時の複雑なデータの取得
移動時には複雑なデータ オブジェクトを渡すのではなく、ナビゲーション アクションの実行時に引数として必要最低限の情報(一意の識別子やその他の形式の ID など)を渡すことを強くおすすめします。
// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")
複雑なオブジェクトは信頼できる単一の情報源(データレイヤなど)にデータとして保存する必要があります。移動後にデスティネーションに到達したら、渡された ID を使用して、信頼できる単一の情報源から必要な情報を読み込むことができます。データレイヤへのアクセスを担う ViewModel で引数を取得するには、ViewModel’s
SavedStateHandle
を使用できます。
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val userId: String = checkNotNull(savedStateHandle["userId"])
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)
// …
}
この手法により、設定の変更時のデータ損失や、対象のオブジェクトの更新時や変更時の不整合を防ぐことができます。
複雑なデータを引数として渡すのを避けるべき理由についての詳しい説明と、サポートされる引数タイプのリストについては、デスティネーション間でデータを渡すをご覧ください。
省略可能な引数の追加
Navigation Compose は、省略可能なナビゲーション引数もサポートしています。省略可能な引数は、次の 2 つの点で必須の引数とは異なります。
- クエリ パラメータの構文(
"?argName={argName}"
)を使用して指定する必要があります。 defaultValue
を設定するか、nullability = true
(デフォルト値を暗黙的にnull
に設定)を指定する必要があります。
つまり、省略可能なすべての引数をリストとして composable()
関数に明示的に追加する必要があります。
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
デスティネーションに渡される引数がない場合でも、代わりに defaultValue
の「user1234」が使用されます。
ルートを通じて引数を処理するという構造のため、コンポーザブルは Navigation から完全に独立しており、テストがかなり容易になります。
ディープリンク
Navigation Compose は、composable()
関数の一部として定義できる暗黙的なディープリンクもサポートしています。その deepLinks
パラメータは、navDeepLink
メソッドを使用してすばやく作成できる NavDeepLink
のリストを受け取ります。
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
ディープリンクを使用すると、特定の URL、アクション、MIME タイプをコンポーザブルに関連付けることができます。デフォルトでは、これらのディープリンクは外部アプリには公開されません。これらのディープリンクを外部で使用できるようにするには、適切な <intent-filter>
要素をアプリの manifest.xml
ファイルに追加する必要があります。上記のディープリンクを有効にするには、マニフェストの <activity>
要素内に次の要素を追加します。
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
別のアプリによってディープリンクがトリガーされると、Navigation はそのコンポーザブルに自動的にディープリンクします。
この同じディープリンクを使用して、コンポーザブルの適切なディープリンクを含む PendingIntent
を作成することもできます。
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
この deepLinkPendingIntent
を他の PendingIntent
と同様に使用して、ディープリンクのデスティネーションでアプリを開くことができます。
ネスト ナビゲーション
デスティネーションをネストグラフにグループ化して、アプリの UI 内の特定のフローをモジュール化できます。その例として、自己完結型のログインフローがあります。
ネストグラフはデスティネーションをカプセル化します。ルートグラフと同様、ネストグラフには、ルートによって開始デスティネーションとして識別されるデスティネーションが必要です。これは、ネストグラフに関連付けられたルートに移動するときに誘導されるデスティネーションです。
ネストグラフを NavHost
に追加するには、navigation
拡張関数を使用します。
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
グラフのサイズが大きくなる場合は、ナビゲーション グラフを複数のメソッドに分割することを強くおすすめします。これにより、複数のモジュールがそれぞれ独自のナビゲーション グラフを提供できるようになります。
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
}
メソッドを NavGraphBuilder
の拡張メソッドにすると、ビルド済みの navigation
、composable
、dialog
拡張メソッドと併せて使用できます。
NavHost(navController, startDestination = "home") {
...
loginGraph(navController)
...
}
下部のナビゲーション バーとの統合
コンポーザブルの階層内の上位のレベルで NavController
を定義することで、Navigation を他のコンポーネント(ボトム ナビゲーション コンポーネントなど)と接続できます。これにより、下部のバーのアイコンを選択して移動できるようになります。
BottomNavigation
コンポーネントと BottomNavigationItem
コンポーネントを使用するには、androidx.compose.material
の依存関係を Android アプリに追加します。
Groovy
dependencies { implementation "androidx.compose.material:material:1.3.1" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.4.2" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.3.1") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.4.2" } kotlinOptions { jvmTarget = "1.8" } }
下部のナビゲーション バーのアイテムをナビゲーション グラフ内のルートにリンクするには、デスティネーションのルートと文字列のリソース ID を含むシールクラス(下記の Screen
など)を定義することをおすすめします。
sealed class Screen(val route: String, @StringRes val resourceId: Int) {
object Profile : Screen("profile", R.string.profile)
object FriendsList : Screen("friendslist", R.string.friends_list)
}
次に、そのアイテムを BottomNavigationItem
が使用可能なリスト内に配置します。
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
BottomNavigation
コンポーザブルで、currentBackStackEntryAsState()
関数を使用して現在の NavBackStackEntry
を取得します。このエントリにより、現在の NavDestination
にアクセスできるようになります。NavDestination
hierarchy を通じて、アイテムのルートを現在のデスティネーションおよびその親デスティネーション(ネストされたナビゲーションを使用しているケースに対応するため)のルートと比較することで、各 BottomNavigationItem
の選択された状態を判定できます。
アイテムをタップするとそのアイテムに移動するように、onClick
ラムダを navigate
の呼び出しに接続するためにもアイテムのルートが使用されます。saveState
と restoreState
のフラグを使用すると、ボトム ナビゲーション アイテムを切り替えるときに、そのアイテムの状態とバックスタックが正しく保存され、復元されます。
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
composable(Screen.Profile.route) { Profile(navController) }
composable(Screen.FriendsList.route) { FriendsList(navController) }
}
}
ここでは、NavController.currentBackStackEntryAsState()
メソッドを使用して NavHost
関数から navController
の状態をホイスティングし、BottomNavigation
コンポーネントと共有しています。つまり、BottomNavigation
は自動的に最新の状態を保持します。
Navigation Compose での型安全性
このページのコードはタイプセーフではありません。存在しないルートまたは誤った引数を使用して navigate()
関数を呼び出すことも可能です。ただし、実行時にタイプセーフになるように Navigation コードを構成することは可能です。これにより次のことが確保され、クラッシュが回避されます。
- デスティネーションまたはナビゲーション グラフに移動するときに指定する引数は適切な型であり、必要な引数がすべて存在している。
SavedStateHandle
から取得する引数は正しい型である。
詳しくは、Navigation の型安全性に関するドキュメントをご覧ください。
相互運用性
Compose で Navigation コンポーネントを使用するには、次の 2 つの方法があります。
- フラグメントに Navigation コンポーネントを使用して、ナビゲーション グラフを定義します。
- Compose のデスティネーションを使用して、Compose 内の
NavHost
でナビゲーション グラフを定義します。これは、ナビゲーション グラフ内のすべての画面がコンポーザブルである場合にのみ可能です。
したがって、Compose アプリと View アプリを組み合わせる場合は、フラグメント ベースの Navigation コンポーネントを使用することをおすすめします。フラグメントは、View ベースの画面、Compose の画面、および View と Compose の両方を使用する画面を保持します。各フラグメントのコンテンツが Compose に組み込まれたら、次のステップでは、これらすべての画面を Navigation Compose に関連付けて、すべてのフラグメントを削除します。
フラグメントに Navigation を使用して Compose から移動する
Compose コード内のデスティネーションを変更するには、階層内の任意のコンポーザブルに渡してそのコンポーザブルからトリガーできるイベントを公開します。
@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
フラグメント内で、NavController
を見つけてデスティネーションに移動することにより、Compose とフラグメント ベースの Navigation コンポーネントを橋渡しします。
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
または、NavController
を Compose 階層の上から下に渡すこともできます。ただし、シンプルな関数を公開すると、再利用とテストがより簡単になります。
テスト
Navigation のコードをコンポーザブルのデスティネーションから分離して、NavHost
コンポーザブルとは別に、各コンポーザブルを個別にテストできるようにすることを強くおすすめします。
つまり、navController
を任意のコンポーザブルに直接渡すのではなく、ナビゲーション コールバックをパラメータとして渡す必要があります。これにより、テストで navController
のインスタンスが不要になるため、すべてのコンポーザブルを個別にテストできます。
composable
ラムダによって一定のレベルの間接性が提供されるため、Navigation のコードをコンポーザブル自体と分離できるようになります。このことは次の 2 つの方向に機能します。
- 解析された引数のみをコンポーザブルに渡します。
NavController
自体ではなく、移動するコンポーザブルによってトリガーされるラムダを渡します。
たとえば、userId
を入力として取り、ユーザーに友だちのプロフィール ページへの移動を許可する Profile
コンポーザブルの場合、次のようなシグネチャになります。
@Composable
fun Profile(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
このように、Profile
コンポーザブルは Navigation とは独立して動作するため、独立してテストできます。composable
ラムダは、Navigation API とコンポーザブルの間のギャップを埋めるために必要な最小限のロジックをカプセル化することになります。
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
navController.navigate("profile?userId=$friendUserId")
}
}
NavHost
、コンポーザブルに渡されるナビゲーション アクション、個々の画面コンポーザブルをテストすることにより、アプリ ナビゲーションの要件に対応するテストを作成することをおすすめします。
NavHost
のテスト
NavHost
のテストを開始するには、次の navigation-testing の依存関係を追加します。
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
NavHost
のテスト対象を設定し、navController
インスタンスのインスタンスをその対象に渡します。このために、Navigation テスト アーティファクトには TestNavHostController
が用意されています。アプリの開始デスティネーションと NavHost
を検証する UI テストは次のようになります。
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Before
fun setupAppNavHost() {
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavHost(navController = navController)
}
}
// Unit test
@Test
fun appNavHost_verifyStartDestination() {
composeTestRule
.onNodeWithContentDescription("Start Screen")
.assertIsDisplayed()
}
}
ナビゲーション アクションのテスト
ナビゲーションの実装は複数の方法でテストできます。UI 要素をクリックして表示されたデスティネーションを検証する方法や、想定されるルートと現在のルートを比較する方法があります。
具体的なアプリの実装をテストする場合は、UI をクリックすることをおすすめします。このテスト方法と、個々のコンポーズ可能な関数を個別にテストする方法については、Jetpack Compose でのテスト Codelab をご覧ください。
navController
(navController
の currentBackStackEntry
)を使用して、現在の String ルートと想定されるルートを比較することで、アサーションを確認することもできます。
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
val route = navController.currentBackStackEntry?.destination?.route
assertEquals(route, "profiles")
}
Compose のテストの基本の詳細については、Compose テストのドキュメントと Jetpack Compose でのテスト Codelab をご覧ください。ナビゲーション コードの高度なテストについて詳しくは、ナビゲーションをテストするのガイドをご覧ください。
詳細
Jetpack Navigation の詳細については、Navigation コンポーネント スタートガイドを参照するか、Jetpack Compose ナビゲーション Codelab をご覧ください。
さまざまな画面サイズ、向き、フォーム ファクタに適応するようにアプリのナビゲーションを設計する方法については、レスポンシブ UI のナビゲーションをご覧ください。
モジュール化されたアプリでの Navigation Compose のより高度な実装(ネストされたグラフや下部のナビゲーション バーの統合などの概念)について詳しくは、Now in Android リポジトリをご覧ください。