Kotlin DSL 和 Navigation Compose 中的類型安全

本頁會說明針對 Navigation Kotlin DSLNavigation Compose 提供執行階段類型安全的最佳做法。總結來說,您的應用程式或導覽圖的每個畫面都應依據個別模組對應至 Navigation 檔案。產生的每個檔案都應含有指定目的地的所有 Navigation 相關資訊。這些導覽檔案也是 Kotlin 瀏覽權限修飾詞用來提供執行階段類型安全的地方:

  • 系統會將類型安全函式公開揭露給程式碼集的其餘部分。
  • 特定畫面或導覽圖的 Navigation 專屬概念會放在同一檔案中,並維持不公開,確保程式碼集的基餘部分無法存取。

分割導覽圖

建議您按畫面分割導覽圖。這和將畫面分割成不同可組合函式的方法大致相同。每個畫面都應有 NavGraphBuilder 擴充功能函式。

這項擴充功能函式是無狀態畫面層級可組合函式和 Navigation 專屬邏輯之間的橋樑。此資料層也可定義該狀態的來源與事件的處理方式。

以下是可以設為自身 internal 模組的一般 ConversationScreen,這樣一來其他模組就無法存取:

// ConversationScreen.kt

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

下列 NavGraphBuilder 擴充功能函式會將 ConversationScreen 可組合項新增為 NavGraph 的目的地,也會將畫面連結至提供畫面 UI 狀態的 ViewModel,並處理畫面相關的商業邏輯。無法由 ViewModel 處理的 Navigation 事件會揭露給呼叫端。

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

ConversationNavigation.kt 檔案會將程式碼從 Navigation 程式庫與目的地本身拆分出來,還會為 Navigation 概念 (例如設為不公開的路徑或引數 ID) 提供封裝,因為這些概念不得洩露到這個檔案之外。無法在這個資料層處理的 Navigation 事件必須向呼叫端揭露,系統才能在正確的層級處理這些事件。您可以在上方程式碼片段中找到包含 onNavigateToParticipantList 的事件範例。

類型安全導覽

Navigation Compose 的建構基礎 Navigation Kotlin DSL「目前」不提供 Safe Args 提供給內建於導覽 XML 資源檔案中的導覽圖的那種編譯時間類型安全。Safe Args 會產生程式碼,其中包含針對 Navigation 目的地和動作的類型安全類別和方法。不過,您可以在執行階段將 Navigation 程式碼建構為類型安全。這樣做可避免當機,同時確保下列事項:

  • 前往目的地或導覽圖時,您提供的引數會是正確類型,且所有必要引數都會呈現。
  • 您從 SavedStateHandle 擷取的引數是正確的類型。

每個目的地也都應該揭露 NavController 擴充功能函式,以便讓其他目的地能安全地前往該目的地。

// ConversationNavigation.kt

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

如要前往您應用程式中含有其他 NavOptions 的畫面 (例如 popUpTo, savedState, restoreStatesingleTop),請在導覽時傳遞選用參數至 NavController 擴充功能函式。

// HomeNavigation.kt

const val HomeRoute = "home"

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

類型安全引數包裝函式

您可以視需要建立類型安全包裝函式,以便為 ViewModel 從 SavedStateHandle 擷取引數,也從目的地內容中的 NavBackStackEntry 擷取引數,藉此獲得本章節簡介中提供的好處。

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

組合導覽圖

導覽圖會使用上述的類型安全擴充功能函式新增目的地,並前往這些目的地。

在以下範例中,「對話」目的地和另外兩個目的地 (「主畫面」和「參與者名單」) 會一起納入到一個應用程式層級 NavHost,如下所示:

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

巢狀導覽圖中的類型安全

請為提供多個畫面的模組選擇正確的瀏覽權限。這個概念與以上章節中各方法的概念相同。不過,將個別畫面完全揭露給其他模組可能並不合理。在這種情況下,建議您將這些畫面視為範圍更大的獨立流程中的一部分。

這組獨立畫面稱為巢狀導覽圖,可讓您將多個畫面納入到單一 NavGraphBuilder 擴充功能方法中。這個方法接著會逐一使用這些 NavController 擴充功能方法將同一模組中的各個畫面串連在一起。

在以下範例中,上一節所述的「對話」目的地會顯示在巢狀導覽圖中,與另外兩個目的地 (「對話清單」和「參與者清單」) 並列:

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

您可以在應用程式層級 NavHost 中使用多個巢狀結構導覽圖,如下所示:

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