Navigation コンポーネントは Jetpack Compose アプリをサポートしています。Navigation コンポーネントのインフラストラクチャと機能を活用しながら、コンポーザブル間を移動できます。
設定
Compose をサポートするには、アプリ モジュールの build.gradle
ファイルで次の依存関係を使用します。
Groovy
dependencies { def nav_version = "2.8.0" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.0" implementation("androidx.navigation:navigation-compose:$nav_version") }
始める
アプリにナビゲーションを実装する場合は、ナビゲーション ホスト、グラフ、コントローラを実装します。詳細については、ナビゲーションの概要をご覧ください。
NavController を作成する
Compose で NavController
を作成する方法については、ナビゲーション コントローラを作成するの Compose セクションをご覧ください。
NavHost を作成する
Compose で NavHost
を作成する方法については、ナビゲーション グラフを設計するの Compose セクションをご覧ください。
コンポーザブルに移動する
コンポーザブルに移動する方法については、アーキテクチャのドキュメントのデスティネーションに移動するをご覧ください。
引数を使用して移動する
コンポーザブル デスティネーション間で引数を渡す方法については、ナビゲーション グラフを設計するの Compose セクションをご覧ください。
操作時に複雑なデータを取得
移動時には複雑なデータ オブジェクトを渡すのではなく、ナビゲーション アクションの実行時に引数として必要最低限の情報(一意の識別子やその他の形式の ID など)を渡すことを強くおすすめします。
// Pass only the user ID when navigating to a new destination as argument
navController.navigate(Profile(id = "user1234"))
複雑なオブジェクトは、データレイヤーなど、信頼できる唯一の情報源にデータとして保存する必要があります。移動後にデスティネーションに到達したら、渡された ID を使用して、信頼できる単一の情報源から必要な情報を読み込むことができます。データレイヤへのアクセスを担う ViewModel
で引数を取得するには、ViewModel
の SavedStateHandle
を使用します。
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val profile = savedStateHandle.toRoute<Profile>()
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)
// …
}
この手法により、設定の変更時のデータ損失や、対象のオブジェクトの更新時や変更時の不整合を防ぐことができます。
複雑なデータを引数として渡すのを避けるべき理由についての詳しい説明と、サポートされる引数タイプのリストについては、デスティネーション間でデータを渡すをご覧ください。
ディープリンク
Navigation Compose は、composable()
関数の一部として定義できるディープリンクもサポートしています。その deepLinks
パラメータは、navDeepLink()
メソッドを使用してすばやく作成できる NavDeepLink
オブジェクトのリストを受け取ります。
@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"
composable<Profile>(
deepLinks = listOf(
navDeepLink<Profile>(basePath = "$uri/profile")
)
) { backStackEntry ->
ProfileScreen(id = backStackEntry.toRoute<Profile>().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/profile/$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.7.5" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.7.5") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
下部のナビゲーション バーのアイテムをナビゲーション グラフ内のルートにリンクするには、ルートクラスとアイコンを持つクラス(下記の TopLevelRoute
など)を定義することをおすすめします。
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
次に、これらのルートを BottomNavigationItem
が使用できるリストに配置します。
val topLevelRoutes = listOf(
TopLevelRoute("Profile", Profile, Icons.Profile),
TopLevelRoute("Friends", Friends, Icons.Friends)
)
BottomNavigation
コンポーザブルで、currentBackStackEntryAsState()
関数を使用して現在の NavBackStackEntry
を取得します。このエントリにより、現在の NavDestination
にアクセスできるようになります。NavDestination
階層を使用して、アイテムのルートを現在のデスティネーションおよびその親デスティネーションのルートと比較することで、各 BottomNavigationItem
の選択された状態を判定できます。これにより、ネストされたナビゲーションを使用しているケースに対応できます。
アイテムをタップするとそのアイテムに移動するように、onClick
ラムダを navigate
の呼び出しに接続するためにもアイテムのルートが使用されます。saveState
と restoreState
のフラグを使用すると、ボトム ナビゲーション アイテムを切り替えるときに、そのアイテムの状態とバックスタックが正しく保存され、復元されます。
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
topLevelRoutes.forEach { topLevelRoute ->
BottomNavigationItem(
icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },
label = { Text(topLevelRoute.name) },
selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,
onClick = {
navController.navigate(topLevelRoute.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 = Profile, Modifier.padding(innerPadding)) {
composable<Profile> { ProfileScreen(...) }
composable<Friends> { FriendsScreen(...) }
}
}
ここでは、NavController.currentBackStackEntryAsState()
メソッドを使用して NavHost
関数から navController
の状態をホイスティングし、BottomNavigation
コンポーネントと共有しています。つまり、BottomNavigation
は自動的に最新の状態を保持します。
相互運用性
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) -> Unit) {
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
を入力として受け取り、ユーザーが友だちのプロフィール ページに移動できるようにする ProfileScreen
コンポーザブルのシグネチャは次のようになります。
@Composable
fun ProfileScreen(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
このように、ProfileScreen
コンポーザブルは Navigation とは独立して動作するため、独立してテストできます。composable
ラムダは、Navigation API とコンポーザブルの間のギャップを埋めるために必要な最小限のロジックをカプセル化することになります。
@Serializable data class Profile(id: String)
composable<Profile> { backStackEntry ->
val profile = backStackEntry.toRoute<Profile>()
ProfileScreen(userId = profile.id) { friendUserId ->
navController.navigate(route = Profile(id = friendUserId))
}
}
NavHost
、コンポーザブルに渡されるナビゲーション アクション、個々の画面コンポーザブルをテストすることにより、アプリ ナビゲーションの要件に対応するテストを作成することをおすすめします。
NavHost
のテスト
NavHost
のテストを開始するには、次の navigation-testing の依存関係を追加します。
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
アプリの NavHost
を、NavHostController
をパラメータとして受け取るコンポーザブルでラップします。
@Composable
fun AppNavHost(navController: NavHostController){
NavHost(navController = navController){ ... }
}
これで、ナビゲーション テスト アーティファクト TestNavHostController
のインスタンスを渡すことで、AppNavHost
と NavHost
内で定義されたすべてのナビゲーション ロジックをテストできます。アプリの開始デスティネーションと 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
)を使用して、現在のルートと想定されるルートを比較することで、アサーションを確認することもできます。
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}
Compose のテストの基本の詳細については、Compose レイアウトのテストと Jetpack Compose でのテストの Codelab をご覧ください。ナビゲーション コードの高度なテストについて詳しくは、ナビゲーションをテストするのガイドをご覧ください。
詳細
Jetpack Navigation の詳細については、Navigation コンポーネント スタートガイドを参照するか、Jetpack Compose ナビゲーション Codelab をご覧ください。
さまざまな画面サイズ、向き、フォーム ファクタに適応するようにアプリのナビゲーションを設計する方法については、レスポンシブ UI のナビゲーションをご覧ください。
ネストされたグラフやボトム ナビゲーション バーの統合などのコンセプトなど、モジュール化されたアプリでのより高度な Compose ナビゲーション実装については、GitHub の Now in Android アプリをご覧ください。
サンプル
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- Compose のマテリアル デザイン 2
- Jetpack Navigation を Navigation Compose に移行する
- 状態をホイスティングする場所