ナビゲーション コードをカプセル化する

Kotlin DSL を使用してグラフを作成する場合、デスティネーションとナビゲーション イベントを 1 つのファイル内に維持するのは難しいことがあります。これは、独立した特徴が複数ある場合に特に当てはまります。

リンク先の抽出

デスティネーションを NavGraphBuilder 拡張関数に移動する必要があります。表示場所を定義するルートと、表示する画面の近くに表示する必要があります。たとえば、連絡先のリストを表示するデスティネーションを作成するアプリレベルのコードについて考えてみましょう。

// MyApp.kt

@Serializable
object Contacts

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
    composable<Contacts> { ContactsScreen( /* ... */ ) }
  }
}

ナビゲーション固有のコードを別のファイルに移動する必要があります。

// ContactsNavigation.kt

@Serializable
object Contacts

fun NavGraphBuilder.contactsDestination() {
    composable<Contacts> { ContactsScreen( /* ... */ ) }
}

// MyApp.kt

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
     contactsDestination()
  }
}

ルートと宛先の定義がメインアプリから分離され、個別に更新できるようになりました。メインアプリは 1 つの拡張関数にのみ依存します。この場合は NavGraphBuilder.contactsDestination() です。

NavGraphBuilder 拡張関数は、ステートレスの画面レベルのコンポーズ可能な関数と Navigation 固有のロジック間のブリッジを形成します。このレイヤでは、状態の取得元とイベントの処理方法も定義できます。

次のスニペットは、連絡先の詳細を表示する新しいデスティネーションを導入し、既存の連絡先リストのデスティネーションを更新して、連絡先の詳細を表示するナビゲーション イベントを公開します。

以下は、他のモジュールがアクセスできないように、独自のモジュールに対して internal できる、一般的な画面セットです。

// ContactScreens.kt

// Displays a list of contacts
@Composable
internal fun ContactsScreen(
  uiState: ContactsUiState,
  onNavigateToContactDetails: (contactId: String) -> Unit
) { ... }

// Displays the details for an individual contact
@Composable
internal fun ContactDetailsScreen(contact: ContactDetails) { ... }

デスティネーションを作成する

次の NavGraphBuilder 拡張関数は、ConversationScreen コンポーザブルを表示するデスティネーションを作成します。さらに、画面 UI の状態を提供し、画面関連のビジネス ロジックを処理する ViewModel に画面を接続できるようになりました。

連絡先情報のデスティネーションへの移動などのナビゲーション イベントは、ViewModel で処理されず、呼び出し元に公開されます。

// ContactsNavigation.kt

@Serializable
object Contacts

// Adds contacts destination to `this` NavGraphBuilder
fun NavGraphBuilder.contactsDestination(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToContactDetails: (contactId: String) -> Unit
) {
  composable<Contacts> {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ContactsViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ContactsScreen(
      uiState,
      onNavigateToContactDetails
    )
  }
}

同じ方法で、ContactDetailsScreen を表示するデスティネーションを作成できます。この場合、ビューモデルから UI 状態を取得する代わりに、NavBackStackEntry から直接取得できます。

// ContactsNavigation.kt

@Serializable
internal data class ContactDetails(val id: String)

fun NavGraphBuilder.contactDetailsScreen() {
  composable<ContactDetails> { navBackStackEntry ->
    ContactDetailsScreen(contact = navBackStackEntry.toRoute())
  }
}

ナビゲーション イベントをカプセル化する

デスティネーションをカプセル化する方法と同じ方法で、ナビゲーション イベントをカプセル化することで、ルートタイプが不必要に公開されるのを防ぐことができます。そのためには、NavController に拡張関数を作成します。

// ContactsNavigation.kt

fun NavController.navigateToContactDetails(id: String) {
  navigate(route = ContactDetails(id = id))
}

まとめ

連絡先を表示するためのナビゲーション コードが、アプリのナビゲーション グラフから明確に分離されました。アプリは次のことを行う必要があります。

  • NavGraphBuilder 拡張関数を呼び出してデスティネーションを作成する
  • ナビゲーション イベントに対して NavController 拡張関数を呼び出して、これらのデスティネーションを接続する
// MyApp.kt

@Composable
fun MyApp() {
  ...
  NavHost(navController, startDestination = Contacts) {
     contactsDestination(onNavigateToContactDetails = { contactId ->
        navController.navigateToContactDetails(id = contactId)
     })
     contactDetailsDestination()
  }
}

まとめ

  • 関連する画面セットのナビゲーション コードを別のファイルに配置してカプセル化する
  • NavGraphBuilder に拡張関数を作成してデスティネーションを公開する
  • NavController で拡張関数を作成してナビゲーション イベントを公開する
  • internal を使用して画面とルートタイプを非公開にする