Encapsuler votre code de navigation

Lorsque vous utilisez le langage DSL Kotlin pour créer votre graphique, gardez les destinations et les événements de navigation dans un seul fichier peut être difficile à gérer. C'est surtout si vous avez plusieurs caractéristiques indépendantes.

Extraire les destinations

Vous devez déplacer vos destinations dans l'extension NavGraphBuilder fonctions. Ils doivent se trouver à proximité des routes qui les définissent des écrans qu'ils affichent. Prenons l'exemple du code suivant au niveau de l'application : qui crée une destination qui affiche une liste de contacts:

// MyApp.kt

@Serializable
object Contacts

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

Vous devez déplacer le code spécifique à la navigation dans un fichier distinct:

// ContactsNavigation.kt

@Serializable
object Contacts

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

// MyApp.kt

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

Les définitions des itinéraires et des destinations sont désormais distinctes de l'application principale. vous pouvez les mettre à jour indépendamment. L'application principale ne dépend que d'un seul fonction d'extension. Dans ce cas, il s’agit NavGraphBuilder.contactsDestination()

La fonction d'extension NavGraphBuilder constitue le pont entre un modèle sans état Fonction composable au niveau de l'écran et logique spécifique à la navigation. Cette couche peut définissent également d'où vient l'état et comment vous gérez les événements.

Exemple

L'extrait de code suivant introduit une nouvelle destination pour afficher les coordonnées et met à jour la destination existante de la liste de contacts pour exposer événement de navigation pour afficher les détails du contact.

Voici un ensemble type d'écrans qui peuvent être internal par rapport à leur propre module. que les autres modules ne peuvent pas y accéder:

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

Créer des destinations

La fonction d'extension NavGraphBuilder suivante crée une destination qui affiche le composable ContactsScreen. De plus, il connecte désormais l'écran avec un ViewModel qui fournit l'état de l'UI de l'écran et gère les la logique métier liée à l'écran.

Les événements de navigation, comme l'accès à la destination des coordonnées, sont exposées à l'appelant plutôt que gérées par 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
    )
  }
}

Vous pouvez utiliser la même approche pour créer une destination qui affiche ContactDetailsScreen Dans ce cas, au lieu d'obtenir l'état de l'UI à partir d'un vous pouvez l'obtenir directement à partir de NavBackStackEntry.

// ContactsNavigation.kt

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

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

Encapsuler les événements de navigation

De la même manière que vous encapsulez des destinations, vous pouvez encapsuler pour éviter d'exposer inutilement des types d'itinéraires. Pour ce faire, procédez comme suit : en créant des fonctions d'extension sur NavController.

// ContactsNavigation.kt

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

Réunissez

Le code de navigation permettant d'afficher les contacts est désormais clairement séparé du graphique de navigation de l'application. L'application doit:

  • Appeler des fonctions d'extension NavGraphBuilder pour créer des destinations
  • Connecter ces destinations en appelant des fonctions d'extension NavController pour les événements de navigation
// MyApp.kt

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

En résumé

  • Encapsulez votre code de navigation pour un ensemble d'écrans associé en le plaçant dans un fichier distinct
  • Exposez des destinations en créant des fonctions d'extension sur NavGraphBuilder
  • Exposer des événements de navigation en créant des fonctions d'extension sur NavController
  • Utiliser internal pour préserver la confidentialité des écrans et des types d'itinéraires