Sûreté du typage dans le DSL Kotlin et Navigation Compose

Cette page présente les bonnes pratiques à suivre pour assurer la sûreté du typage d'exécution dans le DSL Kotlin de navigation et Navigation Compose. En résumé, vous devez mapper chaque écran de votre application ou graphique de navigation à un fichier de navigation par module. Les fichiers obtenus doivent contenir toutes les informations liées à la navigation pour la destination donnée. C'est également dans ces fichiers de navigation que les modificateurs de visibilité Kotlin assurent la sûreté du typage d'exécution :

  • Les fonctions de sûreté du typage sont exposées publiquement au reste du codebase.
  • Les concepts spécifiques à la navigation pour un écran ou un graphique de navigation particuliers sont colocalisés et restent privés dans le même fichier afin que le reste du codebase ne puisse pas y accéder.

Diviser le graphique de navigation

Il est conseillé de diviser le graphique de navigation par écran. Il s'agit globalement de la même approche que pour diviser des écrans en différentes fonctions modulables. Chaque écran doit comporter une fonction d'extension NavGraphBuilder.

Cette fonction d'extension fait le lien entre une fonction modulable sans état au niveau de l'écran et une logique spécifique à la navigation. Cette couche peut également définir l'origine de l'état et la manière dont les événements sont traités.

Voici un ConversationScreen type qui peut être internal à son propre module, de sorte que d'autres modules ne puissent pas y accéder :

// ConversationScreen.kt

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

La fonction d'extension NavGraphBuilder suivante ajoute le composable ConversationScreen en tant que destination de ce NavGraph. Il connecte également 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 qui ne peuvent pas être traités par le ViewModel sont exposés à l'appelant.

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

Le fichier ConversationNavigation.kt sépare le code de la bibliothèque de navigation de la destination. Il encapsule également les concepts de navigation tels que les routes ou les ID d'argument qui restent privés, car ils ne doivent jamais être divulgués en dehors de ce fichier. Les événements de navigation qui ne peuvent pas être gérés au niveau de cette couche doivent être exposés à l'appelant pour être gérés au niveau approprié. Vous trouverez un exemple de ce type d'événement avec onNavigateToParticipantList dans l'extrait de code ci-dessus.

Navigation avec sûreté du typage

Le DSL Kotlin de navigation sur lequel repose Navigation Compose ne propose actuellement aucune solution de sûreté du typage au moment de la compilation, du type de celle fournie par Sage Args aux graphiques de navigation intégrés dans des fichiers de ressources XML. Safe Args génère du code contenant des classes et des méthodes de sûreté de typage pour les destinations et les actions de navigation. Cependant, vous pouvez structurer votre code de navigation pour qu'il soit sûr lors de l'exécution. Vous pouvez ainsi éviter les plantages et vous assurer que :

  • les arguments que vous fournissez lorsque vous accédez à une destination ou un graphique de navigation sont de types appropriés, et que tous les arguments requis sont présents ;
  • les arguments que vous récupérez à partir de SavedStateHandle sont de types appropriés.

Chaque destination doit également exposer une fonction d'extension NavController pour permettre à d'autres destinations d'y accéder en toute sécurité.

// ConversationNavigation.kt

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

Si vous souhaitez accéder à un écran de votre application avec des NavOptions différentes, telles que popUpTo, savedState, restoreState ou singleTop, transmettez un paramètre facultatif à la fonction d'extension NavController.

// HomeNavigation.kt

const val HomeRoute = "home"

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

Wrapper d'arguments avec sûreté du typage

Si vous le souhaitez, vous pouvez créer un wrapper sûr pour extraire les arguments de SavedStateHandle pour votre ViewModel ainsi que de NavBackStackEntry dans le contenu d'une destination pour bénéficier des avantages mentionnés dans l'introduction de cette section.

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

Assembler le graphique de navigation

Les graphiques de navigation utilisent les fonctions d'extension sûres décrites ci-dessus pour ajouter des destinations et y accéder.

Dans l'exemple suivant, la destination conversation et deux autres, home (accueil) et participant list (liste des participants), sont incluses à un niveau d'application NavHost comme suit :

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

Sûreté du typage dans les graphiques de navigation imbriqués

Vous devez choisir la bonne visibilité pour les modules qui fournissent plusieurs écrans. Il s'agit du même concept que pour chaque méthode décrite dans les sections ci-dessus. Toutefois, il peut ne pas être judicieux d'exposer des écrans individuels à d'autres modules. Dans ce cas, vous devez les traiter dans le cadre d'un flux plus vaste et autonome.

Cet ensemble autonome d'écrans est appelé graphique de navigation imbriqué. Il vous permet d'inclure plusieurs écrans dans une seule méthode d'extension NavGraphBuilder. Cette méthode utilise ensuite ces méthodes d'extension NavController pour associer les écrans au sein d'un même module.

Dans l'exemple suivant, la destination conversation décrite dans les sections précédentes apparaît dans un graphique de navigation imbriqué, avec deux autres destinations, liste des conversations et liste des participants :

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

Vous pouvez utiliser plusieurs graphiques de navigation imbriqués dans un niveau d'application NavHost comme suit :

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