Keamanan jenis di Kotlin DSL dan Navigation Compose

Halaman ini berisi praktik terbaik yang menyediakan keamanan jenis runtime untuk Navigation Kotlin DSL dan Navigation Compose. Singkatnya, Anda harus memetakan setiap layar aplikasi atau grafik navigasi ke file Navigasi basis per modul. Setiap file yang dihasilkan harus berisi semua informasi terkait Navigasi untuk tujuan yang telah ditentukan. File navigasi ini juga merupakan tempat pengubah visibilitas Kotlin memberikan keamanan jenis runtime:

  • Fungsi keamanan jenis seluruh codelab dapat dilihat secara publik.
  • Konsep khusus navigasi untuk layar atau grafik navigasi tertentu ditempatkan bersama dan disimpan secara pribadi dalam file yang sama agar tidak dapat diakses oleh bagian codebase lainnya.

Memisahkan grafik navigasi

Anda harus memisahkan grafik navigasi berdasarkan layar. Cara ini pada dasarnya merupakan pendekatan yang sama seperti saat Anda membagi layar ke fungsi composable lain. Setiap layar harus memiliki fungsi ekstensi NavGraphBuilder.

Fungsi ekstensi ini adalah jembatan antara fungsi composable tingkat layar stateless dan logika khusus Navigasi. Lapisan ini juga dapat menentukan lokasi asal status dan cara penanganan peristiwa.

Berikut adalah ConversationScreen standar yang dapat berupa internal untuk modulnya sendiri sehingga modul lain tidak dapat mengaksesnya:

// ConversationScreen.kt

@Composable
internal fun ConversationScreen(
  uiState: ConversationUiState,
  onPinConversation: () -> Unit,
  onNavigateToParticipantList: (conversationId: String) -> Unit
) { ... }

Fungsi ekstensi NavGraphBuilder berikut menambahkan composable ConversationScreen sebagai tujuan dari NavGraph tersebut. Fitur ini juga menghubungkan layar dengan ViewModel yang menyediakan status UI layar dan menangani logika bisnis terkait layar. Peristiwa navigasi yang tidak dapat ditangani oleh ViewModel ditampilkan kepada pemanggil.

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

// Adds conversation screen to `this` NavGraphBuilder
fun NavGraphBuilder.conversationScreen(
  // Navigation events are exposed to the caller to be handled at a higher level
  onNavigateToParticipantList: (conversationId: String) -> Unit
) {
  composable("conversation/{$conversationIdArg}") {
    // The ViewModel as a screen level state holder produces the screen
    // UI state and handles business logic for the ConversationScreen
    val viewModel: ConversationViewModel = hiltViewModel()
    val uiState = viewModel.uiState.collectAsStateWithLifecycle()
    ConversationScreen(
      uiState,
      ::viewModel.pinConversation,
      onNavigateToParticipantList
    )
  }
}

File ConversationNavigation.kt memisahkan kode dari library Navigasi dari tujuan itu sendiri. Library ini juga memberikan enkapsulasi seputar konsep Navigasi seperti rute atau ID argumen yang dirahasiakan karena tidak boleh sampai bocor ke luar file ini. Peristiwa navigasi yang tidak dapat ditangani di lapisan ini harus diinformasikan kepada pemanggil agar ditangani di level yang tepat. Anda akan menemukan contoh peristiwa seperti ini dengan onNavigateToParticipantList dalam cuplikan kode di atas.

Navigasi keamanan jenis

Navigation Kotlin DSL yang di-build pada Navigation Compose saat ini tidak menawarkan keamanan jenis waktu kompilasi dari jenis yang disediakan oleh Safe Args ke grafik navigasi yang dibuat di file resource XML navigasi. Safe Args menghasilkan kode yang berisi class dan metode keamanan jenis untuk tujuan dan tindakan Navigasi. Namun, Anda dapat membuat struktur kode Navigasi agar aman dari error jenis saat runtime. Dengan cara ini, Anda dapat menghindari error dan memastikan bahwa:

  • Argumen yang Anda berikan saat membuka grafik navigasi atau tujuan merupakan jenis yang tepat dan semua argumen yang diperlukan ada.
  • Argumen yang Anda ambil dari SavedStateHandle adalah jenis argumen yang tepat.

Setiap tujuan juga harus mengekspos fungsi ekstensi NavController agar tujuan lain dapat membuka tujuan tersebut dengan aman.

// ConversationNavigation.kt

fun NavController.navigateToConversation(conversationId: String) {
    this.navigate("conversation/$conversationId")
}

Jika Anda ingin membuka layar aplikasi dengan NavOptions lain, seperti popUpTo, savedState, restoreState, atau singleTop, saat membuka, teruskan parameter opsional ke fungsi ekstensi NavController.

// HomeNavigation.kt

const val HomeRoute = "home"

fun NavController.navigateToHome(navOptions: NavOptions? = null) {
    this.navigate(HomeRoute, navOptions)
}

Wrapper argumen keamanan jenis

Sebagai pilihan, Anda dapat membuat wrapper keamanan jenis untuk mengekstrak argumen dari SavedStateHandle untuk ViewModel Anda dan dari NavBackStackEntry di konten tujuan untuk mendapatkan manfaat yang disebutkan dalam pengantar bagian ini.

// ConversationNavigation.kt

private const val conversationIdArg = "conversationId"

internal class ConversationArgs(val conversationId: String) {
  constructor(savedStateHandle: SavedStateHandle) :
    this(checkNotNull(savedStateHandle[conversationIdArg]) as String)
}

// ConversationViewModel.kt

internal class ConversationViewModel(...,
  savedStateHandle: SavedStateHandle
) : ViewModel() {
  private val conversationArgs = ConversationArgs(savedStateHandle)
}

Menyusun grafik navigasi

Grafik navigasi menggunakan fungsi ekstensi keamanan jenis yang dijelaskan di atas untuk menambahkan tujuan dan membukanya.

Dalam contoh berikut, tujuan percakapan bersama dengan dua tujuan lainnya, rumah dan daftar peserta, disertakan dalam tingkat aplikasi NavHost sebagai berikut:

// MyApp.kt

@Composable
fun MyApp(modifier: Modifier = Modifier) {
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = HomeRoute,
    modifier = modifier
  ) {

    homeScreen(
      onNavigateToConversation = { conversationId ->
        navController.navigateToConversation(conversationId)
      }
    )

    conversationScreen(
      onNavigateToParticipantList = { conversationId ->
        navController.navigateToParticipantList(conversationId)
      }
    }

    participantListScreen()
}

Keamanan jenis di grafik navigasi bertingkat

Anda harus memilih visibilitas yang tepat untuk modul yang menyediakan beberapa layar. Konsep ini sama dengan konsep untuk setiap metode di bagian di atas. Namun, mungkin akan terasa ganjil jika layar individual diekspos ke modul lain. Dalam hal ini, Anda harus memperlakukannya sebagai bagian dari alur mandiri yang lebih besar.

Kumpulan layar mandiri ini disebut grafik navigasi bertingkat. Dengan begitu, Anda dapat menyertakan beberapa layar ke dalam satu metode ekstensi NavGraphBuilder. Metode ini menggunakan metode ekstensi NavController tersebut secara bergantian untuk menautkan layar dalam modul yang sama secara bersamaan.

Pada contoh berikut, tujuan percakapan yang telah dijelaskan di bagian sebelumnya muncul dalam grafik navigasi bertingkat bersama dua tujuan lainnya, yaitu daftar percakapan dan daftar peserta.

// ConversationGraphNavigation.kt

private val ConversationGraphRoutePattern = "conversation"

fun NavController.navigateToConversationGraph(navOptions: NavOptions? = null) {
  this.navigate(ConversationGraphRoutePattern, navOptions)
}

fun NavGraphBuilder.conversationGraph(navController: NavController) {
  navigation(
    startDestination = ConversationListRoutePattern,
    route = ConversationGraphRoutePattern
  ) {
    conversationListScreen(
      onNavigateToConversation = { conversationId ->
        navController.navigateToConversation(conversationId)
      }
    )
    conversationScreen(
      onNavigateToParticipantList = { conversationId ->
        navController.navigateToParticipantList(conversationId)
      }
    )
    partipantList()
}

Anda dapat menggunakan beberapa grafik navigasi bertingkat di NavHost tingkat aplikasi sebagai berikut:

// MyApp.kt

@Composable
fun MyApp(modifier: Modifier = Modifier) {
  val navController = rememberNavController()
  NavHost(
    navController = navController,
    startDestination = HomeGraphRoutePattern
    modifier = modifier
  ) {
    homeGraph(
      navController,
      onNavigateToConversation = {
        navController.navigateToConversationGraph()
      }
    }
    conversationGraph(navController)
}