Google は、黒人コミュニティに対する人種平等の促進に取り組んでいます。取り組みを見る

Compose を使用して移動する

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

セットアップ

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

dependencies {
    def nav_compose_version = "1.0.0-alpha01"
    implementation "androidx.navigation:navigation-compose:$nav_compose_version"
}

はじめに

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, startDestination = "profile") {
    composable("profile") { Profile(...) }
    composable("friendslist") { FriendsList(...) }
    ...
}

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

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

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

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

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

コンポーザブルの階層内の上位のレベルで 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 を取得します。そのエントリを使用し、NavHostController の一部である KEY_ROUTE 定数を使用して、引数からルートを取得します。このルートを使用して、選択されたアイテムが現在のデスティネーションかどうかを判断します。それに応じて、ラベルを設定する、アイテムをハイライト表示する、ルートが一致しない場合は移動するなどの方法で応答します。

val navController = rememberNavController()
Scaffold(
    bottomBar = {
        BottomNavigation {
            val navBackStackEntry by navController.currentBackStackEntryAsState()
            val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
            items.forEach { screen ->
                BottomNavigationItem(
                    icon = { Icon(Icons.Filled.Favorite) },
                    label = { Text(stringResource(screen.resourceId)) },
                    selected = currentRoute == screen.route,
                    onClick = {
                        // This is the equivalent to popUpTo the start destination
                        navController.popBackStack(navController.graph.startDestination, false)

                        // This if check gives us a "singleTop" behavior where we do not create a
                        // second instance of the composable if we are already on that destination
                        if (currentRoute != screen.route) {
                            navController.navigate(screen.route)
                        }
                    }
                )
            }
        }
    }
) {

    NavHost(navController, startDestination = Screen.Profile.route) {
        composable(Screen.Profile.route) { Profile(navController) }
        composable(Screen.FriendsList.route) { FriendsList(navController) }
    }
}

ここでは、NavController.currentBackStackEntryAsState() メソッドを使用して NavHost 関数から NavController の状態をホイストし、BottomNavigation コンポーネントと共有しています。つまり、BottomNavigation は自動的に最新の状態を取得することになります。

テスト

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 コンポーネント スタートガイドをご覧ください。