Compose を使用したナビゲーション

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

設定

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

Groovy

dependencies {
    implementation "androidx.navigation:navigation-compose:2.4.0-alpha09"
}

Kotlin

dependencies {
    implementation("androidx.navigation:navigation-compose:2.4.0-alpha09")
}

開始方法

NavController は Navigation コンポーネントの中心的な API です。この API はステートフルであり、アプリ内の画面を構成するコンポーザブルのバックスタックと各画面の状態を追跡します。

NavController を作成するには、コンポーザブルで rememberNavController() メソッドを使用します。

val navController = rememberNavController()

コンポーザブルの階層内で、NavController を参照する必要があるすべてのコンポーザブルがアクセスできる位置に NavController を作成する必要があります。これは状態ホイスティングの原則に従っており、NavController と NavController が提供する状態を currentBackStackEntryAsState() を介して使用できるようになります。この状態は、画面の外側にあるコンポーザブルを更新する際に信頼できる情報として使用されます。この機能の例については、下部のナビゲーション バーとの統合をご覧ください。

NavHost の作成

NavController は 1 つの NavHost コンポーザブルに関連付ける必要があります。NavHostNavController にナビゲーション グラフを関連付けます。ナビゲーション グラフではコンポーザブルのデスティネーション(目的地)が指定されており、それらのデスティネーション間を移動できるようになります。コンポーザブル間を移動すると、NavHost のコンテンツは自動的に再コンポーズされます。ナビゲーション グラフ内の各コンポーザブルのデスティネーションには「ルート」が関連付けられています。

NavHost を作成するには、rememberNavController() を使って以前に作成された NavController と、グラフの開始デスティネーションのルートが必要です。NavHost の作成では、Navigation Kotlin DSL のラムダ構文を使用して、ナビゲーション グラフを作成します。composable() メソッドを使用して、ナビゲーション構造に追加できます。このメソッドでは、デスティネーションにリンクするルートとコンポーザブルを指定する必要があります。

NavHost(navController = navController, startDestination = "profile") {
    composable("profile") { Profile(/*...*/) }
    composable("friendslist") { FriendsList(/*...*/) }
    /*...*/
}

ナビゲーション グラフ内のコンポーザブルのデスティネーションに移動するには、navigate() メソッドを使用する必要があります。navigate() は、デスティネーションのルートを表す単一の String パラメータを取ります。ナビゲーション グラフ内でコンポーザブルから移動するには、navigate() を呼び出します。

@Composable
fun Profile(navController: NavController) {
    /*...*/
    Button(onClick = { navController.navigate("friends") }) {
        Text(text = "Navigate next")
    }
    /*...*/
}

再コンポーズのたびに navigate() が呼び出されるのを避けるため、navigate() は、コンポーザブル自体の一部としてではなく、常にコールバックの一部として呼び出してください。

デフォルトでは、navigate() は新しいデスティネーションをバックスタックに追加します。navigate の動作を変更するには、追加のナビゲーション オプションを navigate() 呼び出しにアタッチします。

// Pop everything up to the "home" destination off the back stack before
// navigating to the "friends" destination
navController.navigate(“friends”) {
    popUpTo("home")
}

// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friends" destination
navController.navigate("friends") {
    popUpTo("home") { inclusive = true }
}

// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
    launchSingleTop = true
}

その他のユースケースについては、popUpTo のガイドをご覧ください。

Navigation Compose では、コンポーザブルのデスティネーション間で引数を渡すこともできます。これを行うには、基本のナビゲーション ライブラリを使用する場合にディープリンクに引数を追加する方法と同様に、引数のプレースホルダをルートに追加する必要があります。

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

デフォルトでは、すべての引数が文字列として解析されます。arguments パラメータを使用して type を設定することで、別の型を指定できます。

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

NavArguments は、composable() 関数のラムダで使用可能な NavBackStackEntry から抽出する必要があります。

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

デスティネーションに引数を渡すには、navigate の呼び出し内のプレースホルダに代わって、ルートに値を追加する必要があります。

navController.navigate("profile/user1234")

サポートされる型の一覧については、デスティネーション間でデータを渡すをご覧ください。

省略可能な引数の追加

Navigation Compose は、省略可能なナビゲーション引数もサポートしています。省略可能な引数は、次の 2 つの点で必須の引数とは異なります。

  • クエリ パラメータの構文("?argName={argName}")を使用して指定する必要があります。
  • defaultValue を設定するか、nullability = true(デフォルト値を暗黙的に null に設定)を指定する必要があります。

つまり、省略可能なすべての引数をリストとして composable() 関数に明示的に追加する必要があります。

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

デスティネーションに渡される引数がない場合でも、代わりに defaultValue の「me」が使用されます。

ルートを通じて引数を処理するという構造のため、コンポーザブルは Navigation から完全に独立しており、テストがかなり容易になります。

Navigation Compose は、composable() 関数の一部として定義できる暗黙的なディープリンクもサポートしています。ディープリンクは、navDeepLink() を使用してリストとして追加します。

val uri = "https://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://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 と同様に使用して、ディープリンクのデスティネーションでアプリを開くことができます。

ネスト ナビゲーション

デスティネーションをネストグラフにグループ化して、アプリの UI 内の特定のフローをモジュール化できます。その例として、自己完結型のログインフローがあります。

ネストグラフはデスティネーションをカプセル化します。ルートグラフと同様、ネストグラフには、ルートによって開始デスティネーションとして識別されるデスティネーションが必要です。これは、ネストグラフに関連付けられたルートに移動するときに誘導されるデスティネーションです。

ネストグラフを NavHost に追加するには、navigation 拡張関数を使用します。

NavHost(navController, startDestination = startRoute) {
    ...
    navigation(startDestination = nestedStartRoute, route = nested) {
        composable(nestedStartRoute) { ... }
    }
    ...
}

下部のナビゲーション バーとの統合

コンポーザブルの階層内の上位のレベルで NavController を定義することで、Navigation を他のコンポーネント(BottomNavBar など)と接続できます。これにより、下部のバーのアイコンを選択して移動できるようになります。

下部のナビゲーション バーのアイテムをナビゲーション グラフ内のルートにリンクするには、デスティネーションのルートと文字列のリソース 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 にアクセスできるようになります。hierarchy ヘルパー メソッドを通じて、アイテムのルートを現在のデスティネーションおよびその親デスティネーション(ネストされたナビゲーションを使用しているケースに対応するため)のルートと比較することで、各 BottomNavigationItem の選択された状態を判定できます。

アイテムをタップするとそのアイテムに移動するように、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 は自動的に最新の状態を保持します。

相互運用性

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

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

したがって、ハイブリッド アプリの場合は、フラグメント ベースの Navigation コンポーネントを使用するとともに、ビューベースの画面、Compose の画面、およびビューと Compose の両方を使用する画面を保持するフラグメントを使用することをおすすめします。まず、アプリの各画面フラグメントをコンポーザブルのラッパーにします。次に、それらの画面すべてを一緒に Navigation Compose に関連付けて、すべてのフラグメントを削除します。

Compose コード内のデスティネーションを変更するには、階層内の任意のコンポーザブルに渡してそのコンポーザブルからトリガーできるイベントを公開します。

@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

フラグメント内で、NavController を見つけてデスティネーションに移動することにより、Compose とフラグメント ベースの Navigation コンポーネントを橋渡しします。

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

または、NavController を Compose 階層の上から下に渡すこともできます。ただし、シンプルな関数を公開すると、再利用とテストがより簡単になります。

テスト

Navigation のコードをコンポーザブルのデスティネーションから分離して、NavHost コンポーザブルとは別に、各コンポーザブルを個別にテストできるようにすることを強くおすすめします。

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 = "me" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

詳細

Jetpack Navigation の詳細については、Navigation コンポーネント スタートガイドを参照するか、Jetpack Compose ナビゲーション Codelab をご覧ください。