Compose でのナビゲーション

Navigation コンポーネントは Jetpack Compose アプリをサポートしています。Navigation コンポーネントのインフラストラクチャと機能を活用しながら、コンポーザブル間を移動できます。

設定

Compose をサポートするには、アプリ モジュールの build.gradle ファイルで次の依存関係を使用します。

Groovy

dependencies {
    def nav_version = "2.8.4"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.8.4"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

始める

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

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

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

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 に関連付けて、すべてのフラグメントを削除します。

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 のインスタンスを渡すことで、AppNavHostNavHost 内で定義されたすべてのナビゲーション ロジックをテストできます。アプリの開始デスティネーションと 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)を使用して、現在のルートと想定されるルートを比較することで、アサーションを確認することもできます。

@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 の 最新の Android アプリをご覧ください。

サンプル