Compose でのナビゲーション

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

Compose 専用に構築された最新のプレリリース ナビゲーション ライブラリについては、Navigation 3 のドキュメントをご覧ください。

設定

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

Groovy

dependencies {
    def nav_version = "2.9.6"

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

Kotlin

dependencies {
    val nav_version = "2.9.6"

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

ネスト ナビゲーション

ネストされたナビゲーション グラフの作成方法については、ネストされたグラフをご覧ください。

アダプティブ ナビゲーション バーとナビゲーション レールを作成する

NavigationSuiteScaffold は、現在の WindowSizeClass に基づいて、アプリに適切なトップレベルのナビゲーション UI を表示します。コンパクト画面の場合、スキャフォールドにはボトム ナビゲーション バーが表示され、中程度幅と拡大幅の画面の場合、ナビゲーション レールが表示されます。

NavigationSuiteScaffold はメイン ナビゲーションを処理しますが、アダプティブ レイアウトには他の特殊なコンポーザブルが関与することがよくあります。アダプティブ デザインで一般的なリストと詳細、サポートペインの正規レイアウトには、それぞれ ListDetailPaneScaffoldSupportingPaneScaffold を使用します。詳しくは、アダプティブ レイアウトをビルドするをご覧ください。

相互運用性

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

サンプル