Compose でのナビゲーション

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")
}

始める

アプリにナビゲーションを実装する場合は、ナビゲーション ホスト、グラフ、コントローラを実装します。詳細については、ナビゲーションの概要をご覧ください。

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 内の引数を取得するには、ViewModelSavedStateHandle を使用します。

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 タイプをコンポーザブルに関連付けることができます。デフォルトでは、これらのディープリンクは外部アプリには公開されません。これらのディープリンクを外部で使用できるようにするには、アプリの manifest.xml ファイルに適切な <intent-filter> 要素を追加する必要があります。上記の例でディープリンクを有効にするには、マニフェストの <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.2"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.10"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.6.2")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.10"
    }

    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 の呼び出しに接続するためにもアイテムのルートが使用されます。saveStaterestoreState のフラグを使用すると、ボトム ナビゲーション アイテムを切り替えるときに、そのアイテムの状態とバックスタックが正しく保存され、復元されます。

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 から取得する引数は正しい型である。

詳細については、Kotlin DSL と Navigation Compose での型安全性をご覧ください。

相互運用性

Compose で Navigation コンポーネントを使用するには、次の 2 つの方法があります。

  • フラグメントに Navigation コンポーネントを使用して、ナビゲーション グラフを定義します。
  • Compose のデスティネーションを使用して、Compose 内の NavHost でナビゲーション グラフを定義します。これは、ナビゲーション グラフ内のすべての画面がコンポーザブルである場合にのみ可能です。

したがって、Compose アプリと View アプリを組み合わせる場合は、フラグメント ベースの Navigation コンポーネントを使用することをおすすめします。Fragment は、View ベースの画面、Compose 画面、および View と Compose の両方を使用する画面を保持します。各フラグメントのコンテンツが 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 のテストを開始するには、次の Navigation-testing の依存関係を追加します。

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 をご覧ください。

navControllernavControllercurrentBackStackEntry)を使用して、現在の 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 アプリをご覧ください。

サンプル