Sicurezza dei tipi in Kotlin DSL e Navigation Compose

Questa pagina contiene le best practice per fornire la sicurezza del tipo di runtime a Navigazione Kotlin DSL e Scrittura navigazione. In sintesi, devi mappare ogni schermata dell'app o del grafico di navigazione a un file di navigazione in base al modulo. Ogni file risultante deve contenere tutte le informazioni relative alla navigazione per la destinazione specificata. Inoltre, in questi file di navigazione i modificatori di visibilità Kotlin forniscono la sicurezza dei tipi di runtime:

  • Le funzioni di sicurezza dei tipi sono esposte pubblicamente al resto del codebase.
  • I concetti specifici della navigazione per una determinata schermata o grafico di navigazione vengono collocati nella stessa posizione e mantenuti privati nello stesso file per renderli inaccessibili al resto del codebase.

Dividi il grafico di navigazione

Dovresti suddividere il grafico di navigazione per schermata. Si tratta essenzialmente dello stesso approccio impiegato per suddividere gli schermi in diverse funzioni componibili. Ogni schermata deve avere una funzione di estensione NavGraphBuilder.

Questa funzione di estensione è il ponte tra una funzione componibile stateless a livello di schermo e la logica specifica per la navigazione. Questo livello può anche definire da dove proviene lo stato e come vengono gestiti gli eventi.

Di seguito è riportato un valore ConversationScreen tipico che può essere internal nel proprio modulo, in modo che gli altri moduli non possano accedervi:

// ConversationScreen.kt

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

La seguente funzione dell'estensione NavGraphBuilder aggiunge la componibile ConversationScreen come destinazione di quella NavGraph. Inoltre, connette lo schermo a un ViewModel che fornisce lo stato dell'interfaccia utente dello schermo e gestisce la logica di business relativa allo schermo. Gli eventi di navigazione che non possono essere gestiti da ViewModel sono esposti al chiamante.

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

Il file ConversationNavigation.kt separa il codice della libreria di navigazione dalla destinazione stessa. Fornisce inoltre l'incapsulamento intorno a concetti di navigazione, come route o ID argomento, che vengono mantenuti privati perché non devono mai essere divulgati al di fuori di questo file. Gli eventi di navigazione che non possono essere gestiti a questo livello, devono essere esposti al chiamante in modo che vengano gestiti al giusto livello. Troverai un esempio di questo evento con onNavigateToParticipantList nello snippet di codice riportato sopra.

Digita la navigazione sicura

Il DSL di navigazione Kotlin su cui si basa Navigation Compose non al momento offre sicurezza di tipo in fase di compilazione del tipo Args sicuri forniti per i grafici di navigazione integrati nei file di risorse XML di navigazione. Safe Args genera codice che contiene classi e metodi sicuri per tipo per le destinazioni e le azioni di navigazione. Tuttavia, puoi strutturare il codice di navigazione in modo che sia sicuro per tipo durante l'esecuzione. che ti consente di evitare arresti anomali e di assicurarti che:

  • Gli argomenti forniti quando accedi a una destinazione o a un grafico di navigazione sono dei tipi giusti e che siano presenti tutti gli argomenti richiesti.
  • Gli argomenti recuperati da SavedStateHandle sono i tipi corretti.

Ogni destinazione dovrebbe anche esporre una funzione di estensione NavController per consentire ad altre destinazioni di accedervi in sicurezza.

// ConversationNavigation.kt

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

Se vuoi accedere a una schermata della tua app con un elemento NavOptions diverso, ad esempio popUpTo, savedState, restoreState o singleTop durante la navigazione, trasmetti un parametro facoltativo alla funzione dell'estensione NavController.

// HomeNavigation.kt

const val HomeRoute = "home"

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

Digita wrapper argomenti sicuri

Facoltativamente, puoi creare un wrapper sicuro per tipo per estrarre gli argomenti da un SavedStateHandle per il tuo ViewModel e da un NavBackStackEntry nei contenuti di una destinazione per ottenere i vantaggi menzionati nell'introduzione a questa sezione.

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

Creare il grafico di navigazione

I grafici di navigazione utilizzano le funzioni di estensione sicura descritte sopra per aggiungere destinazioni e raggiungerle.

Nell'esempio seguente, la destinazione conversazione insieme ad altre due destinazioni, home e partecipanti elenco, sono incluse in un livello di app NavHost come segue:

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

Digita safety nei grafici di navigazione nidificati

Dovresti scegliere la visibilità giusta per i moduli che forniscono più schermate. Questo è lo stesso concetto di ogni metodo descritto nelle sezioni precedenti. Tuttavia, potrebbe non avere senso esporre singole schermate ad altri moduli. In questo caso, devi invece trattarle come parte di un flusso più ampio e indipendente.

Questo insieme di schermate indipendenti è chiamato grafico di navigazione nidificato. In questo modo puoi includere più schermate in un singolo metodo di estensione NavGraphBuilder. Questo metodo utilizza questi metodi di estensione NavController a loro volta per collegare insieme le schermate all'interno dello stesso modulo.

Nell'esempio seguente, la destinazione conversazione descritta nelle sezioni precedenti è visualizzata in un grafico di navigazione nidificato insieme ad altre due destinazioni, l'elenco delle conversazioni e l'elenco dei partecipanti:

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

Puoi utilizzare più grafici di navigazione nidificati in un'app NavHost a livello di app nel seguente modo:

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