Navigation コンポーネントは Jetpack Compose アプリをサポートしています。Navigation コンポーネントのインフラストラクチャと機能を活用しながら、コンポーザブル間を移動できます。
設定
Compose をサポートするには、アプリ モジュールの build.gradle
ファイルで次の依存関係を使用します。
dependencies { def nav_version = "2.8.9" implementation "androidx.navigation:navigation-compose:$nav_version" }
dependencies { val nav_version = "2.8.9" 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 アプリに追加します。
dependencies { implementation "androidx.compose.material:material:1.7.8" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
dependencies { implementation("androidx.compose.material:material:1.7.8") } 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 コンポーネントを使用することをおすすめします。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
を入力として受け取り、ユーザーが友だちのプロフィール ページに移動できるようにする 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 アプリをご覧ください。
サンプル
Jetnews is a sample news reading app, built with Jetpack Compose. The goal of the sample is to showcase the current UI capabilities of Compose.
To try out this sample app, use the latest stable version of Android Studio. You can clone this repository Learn how this app was designed and built in the design case study, architecture learning journey and modularization learning journey.
This is the repository for the Now in Android app. It is a work in progress 🚧.
Now in Android is a fully functionalJetnews sample
Now in Android App