Encapsuler votre code de navigation

Lorsque vous utilisez le DSL Kotlin pour construire votre graphe, il peut être difficile de conserver les destinations et les événements de navigation dans un seul fichier. Cela est particulièrement vrai si vous disposez de plusieurs caractéristiques indépendantes.

Extraire les destinations

Vous devez déplacer vos destinations dans les fonctions d'extension NavGraphBuilder. Ils doivent se trouver à proximité des itinéraires qui les définissent et des écrans qu'ils affichent. Prenons l'exemple du code d'application suivant, qui crée une destination affichant 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 propre à 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 routes et les définitions 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'une seule fonction d'extension. Dans ce cas, il s'agit de NavGraphBuilder.contactsDestination().

La fonction d'extension NavGraphBuilder constitue la passerelle entre une fonction modulable sans état au niveau de l'écran et une logique spécifique à Navigation. Cette couche peut également définir la provenance de l'état et la manière dont vous gérez les événements.

Exemple

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

Voici un ensemble type d'écrans pouvant être internal pour leur propre module, de sorte que les autres modules ne puissent 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 ConversationScreen. En outre, il connecte désormais l'écran à un ViewModel qui fournit l'état de l'UI de l'écran et gère la logique métier liée à l'écran.

Les événements de navigation, tels que la navigation vers la destination des coordonnées du contact, sont exposés à l'appelant au lieu d'être gérés 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 modèle de vue, 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 les événements de navigation pour éviter d'exposer inutilement des types d'itinéraires. Pour ce faire, créez des fonctions d'extension sur NavController.

// ContactsNavigation.kt

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

Rassembler

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 les fonctions d'extension NavGraphBuilder pour créer des destinations
  • Associez ces destinations en appelant les 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.
  • Exposer des destinations en créant des fonctions d'extension sur NavGraphBuilder
  • Exposer les é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 de routes