Navigation コンポーネントは、Jetpack Compose アプリをサポートしています。Navigation コンポーネントのインフラストラクチャと機能を活用しながら、コンポーザブル間を移動できます。
セットアップ
Compose をサポートするには、アプリ モジュールの build.gradle
ファイルで次の依存関係を使用します。
Groovy
dependencies { def nav_version = "2.7.7" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.7.7" implementation("androidx.navigation:navigation-compose:$nav_version") }
開始する
アプリにナビゲーションを実装する場合は、ナビゲーション ホスト、グラフ、コントローラを実装します。詳細については、ナビゲーションの概要をご覧ください。
NavController を作成する
Compose で NavController
を作成する方法については、ナビゲーション コントローラを作成するの Compose セクションをご覧ください。
NavHost を作成する
Compose で NavHost
を作成する方法については、ナビゲーション グラフを設計するの Compose セクションをご覧ください。
コンポーザブルに移動する
コンポーザブルへの移動については、アーキテクチャ ドキュメントのデスティネーションに移動するをご覧ください。
引数を使用して移動する
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
の 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
を設定するか、nullable = 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
と同様に使用して、ディープリンクのデスティネーションでアプリを開くことができます。
ネスト ナビゲーション
ネストされたナビゲーション グラフの作成方法については、ネストされたグラフをご覧ください。
下部のナビゲーション バーとの統合
コンポーザブルの階層内の上位のレベルで NavController
を定義することで、Navigation を他のコンポーネント(ボトム ナビゲーション コンポーネントなど)と接続できます。これにより、下部のバーにあるアイコンを選択して移動できるようになります。
BottomNavigation
コンポーネントと BottomNavigationItem
コンポーネントを使用するには、androidx.compose.material
の依存関係を Android アプリに追加します。
Groovy
dependencies { implementation "androidx.compose.material:material:1.6.8" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.14" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.6.8") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.14" } 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
にアクセスできるようになります。各 BottomNavigationItem
の選択状態は、アイテムのルートを現在のデスティネーションおよびその親デスティネーションのルートと比較することで確認できます。これは、NavDestination
階層を使用したネストされたナビゲーションを使用する場合の処理に役立ちます。
アイテムをタップするとそのアイテムに移動するように、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()
関数を呼び出すことができます。ただし、実行時にタイプセーフになるようにナビゲーション コードを構成できます。これにより次のことが確保され、クラッシュが回避されます。
- デスティネーションまたはナビゲーション グラフに移動するときに指定する引数は適切な型であり、必要な引数がすべて存在している。
SavedStateHandle
から取得する引数は正しい型である。
詳細については、Kotlin DSL と Navigation Compose での型安全性をご覧ください。
相互運用性
Compose で Navigation コンポーネントを使用するには、次の 2 つの方法があります。
- フラグメントに Navigation コンポーネントを使用して、ナビゲーション グラフを定義します。
- Compose のデスティネーションを使用して、Compose 内の
NavHost
でナビゲーション グラフを定義します。これは、ナビゲーション グラフ内のすべての画面がコンポーザブルである場合にのみ可能です。
したがって、Compose アプリと View アプリを組み合わせる場合は、フラグメント ベースの Navigation コンポーネントを使用することをおすすめします。Fragment は、ビューベースの画面、Compose 画面、ビューと Compose の両方を使用する画面を保持します。各フラグメントのコンテンツが Compose に追加されたら、次のステップとして、すべての画面を Navigation Compose に結び付け、すべてのフラグメントを削除します。
フラグメントに Navigation を使用して Compose から移動する
Compose コード内のデスティネーションを変更するには、階層内の任意のコンポーザブルに渡してそのコンポーザブルからトリガーできるイベントを公開します。
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
フラグメント内で、NavController
を見つけてデスティネーションに移動することにより、Compose とフラグメント ベースの Navigation コンポーネントを橋渡しします。
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
または、NavController
を Compose 階層の上から下に渡すこともできます。ただし、シンプルな関数を公開すると、再利用とテストがより簡単になります。
テスト
ナビゲーション コードをコンポーザブルのデスティネーションから分離して、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
のテストを開始するには、次のナビゲーション テストの依存関係を追加します。
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
NavHost
のテスト対象を設定し、navController
インスタンスのインスタンスをその対象に渡します。そのために、ナビゲーション テスト アーティファクトは 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 のナビゲーションをご覧ください。
ネストされたグラフやボトム ナビゲーション バーの統合などのコンセプトなど、モジュール化されたアプリでのより高度な Compose ナビゲーション実装については、GitHub の Now in Android アプリをご覧ください。
サンプル
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- Compose のマテリアル デザイン 2
- Jetpack Navigation を Navigation Compose に移行する
- 状態をホイスティングする場所