An toàn về kiểu trong DSL Kotlin và Navigation Compose

Trang này đề cập các phương pháp hay nhất để đảm bảo an toàn về kiểu thời gian chạy cho Navigation Kotlin DSLNavigation Compose. Nhìn chung thì bạn nên ánh xạ từng màn hình của ứng dụng hoặc biểu đồ điều hướng đến tệp Navigation trên cơ sở từng mô-đun. Mọi tệp kết quả đều phải chứa tất cả thông tin Navigation liên quan cho một đích đến nhất định. Các tệp điều hướng này cũng là nơi đối tượng sửa đổi chế độ hiển thị Kotlin cung cấp sự an toàn về kiểu trong thời gian chạy:

  • Các hàm an toàn về kiểu được hiển thị công khai với phần còn lại của cơ sở mã.
  • Các khái niệm cụ thể về điều hướng cho một màn hình hoặc biểu đồ điều hướng cụ thể được đặt vào cùng vị trí và giữ riêng trong cùng một tệp để phần còn lại của cơ sở mã không thể truy cập vào chúng.

Chia tách biểu đồ điều hướng

Bạn nên chia tách biểu đồ điều hướng theo màn hình. Về cơ bản, đây là cách tiếp cận giống như khi chia tách màn hình cho nhiều hàm có khả năng kết hợp. Mỗi màn hình phải có một hàm tiện ích NavGraphBuilder.

Hàm tiện ích này là cầu nối giữa hàm có khả năng kết hợp cấp màn hình không có trạng thái và logic Điều hướng cụ thể. Lớp này cũng có thể xác định nguồn gốc của trạng thái và cách xử lý sự kiện.

Sau đây là một ConversationScreen thông thường, có thể là internal đối với mô-đun của chính nó để các mô-đun khác không thể truy cập vào đó:

// ConversationScreen.kt

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

Hàm tiện ích NavGraphBuilder sau đây thêm thành phần kết hợp ConversationScreen làm đích đến của NavGraph đó. Hàm này cũng kết nối màn hình với ViewModel cung cấp trạng thái giao diện người dùng màn hình và xử lý logic nghiệp vụ liên quan đến màn hình. Sự kiện Navigation không thể được ViewModel xử lý sẽ được hiển thị cho phương thức gọi.

// 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
    )
  }
}

Tệp ConversationNavigation.kt tách mã khỏi thư viện Navigation và chính đích đến này. Tệp này cũng cung cấp thông tin đóng gói về các khái niệm Navigation như tuyến hoặc ID đối số được giữ riêng tư vì các thông tin này không bao giờ được rò rỉ ra bên ngoài tệp này. Các sự kiện Navigation không thể xử lý ở lớp này cần được hiển thị cho phương thức gọi để được xử lý ở cấp phù hợp. Bạn sẽ tìm thấy ví dụ về những sự kiện như vậy nhờ onNavigateToParticipantList trong đoạn mã ở trên.

An toàn về kiểu khi điều hướng

Navigation Kotlin DSL mà Navigation Compose được tạo trên đó hiện không cung cấp chế độ an toàn về kiểu vào thời gian biên dịch theo cách Safe Args cung cấp cho biểu đồ điều hướng tạo trong tệp tài nguyên XML điều hướng. Safe Args tạo mã chứa các lớp và phương thức an toàn về kiểu cho các thao tác và đích đến Navigation. Tuy nhiên, bạn có thể định cấu trúc mã Navigation để được an toàn về kiểu trong thời gian chạy. Nhờ vậy, bạn có thể tránh sự cố và đảm bảo rằng:

  • Các đối số mà bạn cung cấp khi điều hướng đến một đích đến hoặc biểu đồ điều hướng đều có kiểu phù hợp và có mặt đầy đủ.
  • Các đối số bạn truy xuất qua SavedStateHandle đều có kiểu phù hợp.

Mỗi đích đến cũng phải hiển thị hàm tiện ích NavController để cho phép các đích đến khác điều hướng đến nó một cách an toàn.

// ConversationNavigation.kt

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

Nếu muốn điều hướng đến một màn hình của ứng dụng bằng NavOptions khác, chẳng hạn như popUpTo, savedState, restoreState hoặc singleTop khi điều hướng, hãy truyền một tham số không bắt buộc vào hàm tiện ích NavController.

// HomeNavigation.kt

const val HomeRoute = "home"

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

Trình bao bọc đối số an toàn về kiểu

Bạn có thể tuỳ ý tạo một trình bao bọc an toàn về kiểu để trích xuất các đối số từ SavedStateHandle cho ViewModel và từ một NavBackStackEntry trong nội dung của đích đến để nhận được những lợi ích nêu trong phần giới thiệu về phần này.

// 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)
}

Tập hợp biểu đồ điều hướng

Biểu đồ điều hướng sử dụng các hàm tiện ích an toàn về kiểu được mô tả ở trên để thêm đích đến và điều hướng đến các đích đó.

Trong ví dụ sau, đích đến conversation (cuộc trò chuyện) cùng hai đích đến khác là home (trang chủ) và participant list(danh sách người tham gia) được đưa vào trong một NavHost cấp ứng dụng như sau:

// 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()
}

An toàn về kiểu trong các biểu đồ điều hướng lồng

Bạn nên chọn chế độ hiển thị phù hợp cho các mô-đun cung cấp nhiều màn hình. Đây là khái niệm tương tự như đối với mỗi phương thức trong các phần trên. Tuy nhiên, việc hiển thị từng màn hình cho các mô-đun khác có thể sẽ không hợp lý. Trong trường hợp đó, bạn nên coi chúng là một phần của một luồng độc lập lớn hơn.

Tập hợp các màn hình độc lập này được gọi là biểu đồ điều hướng lồng. Điều này cho phép bạn đưa nhiều màn hình vào một phương thức tiện ích NavGraphBuilder duy nhất. Phương thức này sử dụng các phương thức tiện ích NavController đó để liên kết các màn hình trong cùng một mô-đun với nhau.

Trong ví dụ sau, đích đến của conversation (cuộc trò chuyện), được mô tả trong các phần trước, sẽ xuất hiện ở biểu đồ điều hướng lồng cùng với hai đích đến khác, conversation list (danh sách cuộc trò chuyện) và participant list (danh sách người tham gia):

// 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()
}

Có thể sử dụng nhiều biểu đồ điều hướng lồng trong một NavHost cấp ứng dụng như sau:

// 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)
}