Navigation avec Jetpack Compose

1. Présentation

Dernière mise à jour : 17/03/2021

Prérequis

Navigation est une bibliothèque Jetpack qui permet de naviguer d'une destination à une autre dans votre application, selon des chemins spécifiques. La bibliothèque Navigation fournit également un artefact particulier pour permettre une navigation cohérente et unique dans les écrans créés avec Jetpack Compose. Cet artefact (navigation-compose) est le sujet central de cet atelier de programmation.

Objectifs de l'atelier

Vous allez utiliser l'étude Rally Material comme base de cet atelier de programmation. Vous allez transférer le code de navigation existant pour utiliser le composant Navigation de Jetpack afin de naviguer entre les écrans dans Jetpack Compose.

Points abordés

  • Principes de base de l'utilisation de la bibliothèque Navigation de Jetpack avec Jetpack Compose
  • Navigation d'un composable à un autre
  • Navigation avec les arguments obligatoires et facultatifs
  • Navigation à l'aide de liens profonds
  • Intégration d'une barre de tabulation dans votre hiérarchie de navigation
  • Test de la navigation

2. Prérequis

Vous pouvez suivre cet atelier de programmation sur votre ordinateur.

Pour le suivre seul, clonez son point de départ.

$ git clone https://github.com/googlecodelabs/android-compose-codelabs.git

Vous pouvez également télécharger deux fichiers ZIP :

Maintenant que vous avez téléchargé le code, ouvrez le projet NavigationCodelab dans Android Studio. Vous êtes prêt à commencer.

3. Migrer vers Navigation

Rally est une application qui, d'origine, n'utilise pas Navigation. La migration comporte plusieurs étapes :

  1. Ajouter la dépendance de navigation
  2. Configurer NavController et NavHost
  3. Préparer les itinéraires pour les destinations
  4. Remplacer le mécanisme de destination d'origine par des itinéraires de navigation

Ajouter la dépendance de navigation

Ouvrez le fichier de compilation de l'application, qui se trouve ici : app/build.gradle. Dans la section des dépendances, ajoutez la dépendance navigation-compose.

dependencies {
  implementation "androidx.navigation:navigation-compose:2.4.0-beta02"
  // other dependencies
}

Synchronisez le projet. Vous êtes prêt à utiliser Navigation dans Compose.

Configurer NavController

NavController est le composant essentiel pour utiliser Navigation dans Compose. Il effectue le suivi des entrées de la pile "Retour", fait avancer la pile, et permet de manipuler la pile "Retour" et de naviguer entre les états de l'écran. Dans la mesure où NavController est essentiel pour naviguer, vous devez d'abord le créer pour pouvoir naviguer jusqu'aux destinations.

Dans Compose, vous travaillez avec NavHostController, une sous-classe de NavController. Récupérez NavController à l'aide de la fonction rememberNavController(). Le composant NavController est alors créé et mémorisé. Il survit aux modifications de configuration (en utilisant rememberSavable). NavController est associé à un composable NavHost unique. NavHost associe NavController à un graphique de navigation dans lequel les destinations des composables sont spécifiées.

Pour cet atelier de programmation, récupérez et stockez NavController dans RallyApp. C'est le composable racine pour l'ensemble de l'application. Il se trouve dans RallyActivity.kt.

import androidx.navigation.compose.rememberNavController
...

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
        val navController = rememberNavController()
        Scaffold(...
}

Préparer les itinéraires pour les destinations

Présentation

L'application Rally possède trois écrans :

  1. Overview (Aperçu) : présentation de toutes les alertes et transactions financières
  2. Accounts (Comptes) : informations sur les comptes existants
  3. Bills (Factures) : dépenses planifiées

Capture de l'écran Overview (Aperçu) contenant des informations sur les alertes, les comptes et les factures. Capture de l'écran Accounts (Comptes), contenant des informations sur plusieurs comptes. Capture de l'écran "Bills" (Factures) contenant des informations sur plusieurs factures à régler.

Les trois écrans sont conçus à l'aide de composables. Regardez le fichier RallyScreen.kt. Les trois écrans y sont déclarés. Vous mapperez plus tard ces écrans avec des destinations de navigation, avec Overview comme destination de départ. Vous déplacerez également les composables de RallyScreen vers NavHost. Pour le moment, vous pouvez conserver RallyScreen tel quel.

Lorsque vous utilisez Navigation dans Compose, les itinéraires sont représentés sous forme de chaînes. Celles-ci sont semblables à des URL ou à des liens profonds. Dans cet atelier de programmation, nous utiliserons la propriété name de chaque élément RallyScreen comme itinéraire, par exemple, RallyScreen.Overview.name.

Préparation

Revenez au composable RallyApp dans RallyActivity.kt et remplacez Box, qui contient le contenu de l'écran, par le composant NavHost que vous venez de créer. Transmettez le composant navController que nous avons créé à l'étape précédente. NavHost a également besoin d'une startDestination. Définissez-la sur RallyScreen.Overview.name. Créez également un Modifier pour transmettre la marge intérieure à NavHost.

import androidx.compose.foundation.layout.Box
import androidx.compose.material.Scaffold
import androidx.navigation.compose.NavHost
...

Scaffold(...) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name,
            modifier = Modifier.padding(innerPadding)
        ) { ... }

À présent, nous pouvons définir le graphique de navigation. Les destinations auxquelles NavHost peut accéder sont prêtes à accepter des destinations. Pour ce faire, nous utilisons un NavGraphBuilder, qui est fourni au dernier paramètre de NavHost (un lambda pour définir votre graphe). Comme ce paramètre requiert une fonction, vous pouvez déclarer des destinations dans un lambda de fin. L'artefact Navigation de Compose fournit la fonction d'extension NavGraphBuilder.composable. Utilisez-la pour définir les destinations de navigation dans votre graphique.

import androidx.navigation.compose.NavHost
...

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name,
    modifier = Modifier.padding(innerPadding)

) {
    composable(RallyScreen.Overview.name) { ... }
}

Pour l'instant, nous allons définir provisoirement un Text avec le nom de l'écran comme contenu du composable. À l'étape suivante, nous utiliserons les composables existants.

import androidx.compose.material.Text
import androidx.navigation.compose.composable
...

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name
    modifier = Modifier.padding(innerPadding)
) {
    composable(RallyScreen.Overview.name) {
      Text(text = RallyScreen.Overview.name)
    }

    // TODO: Add the other two screens
}

À présent, supprimez l'appel currentScreen.content de Scaffold, puis exécutez l'application. Le nom de la destination de départ et les onglets ci-dessus s'affichent.

Vous devriez obtenir un objet NavHost semblable à celui-ci :

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name,
    modifier = Modifier.padding(innerPadding)
) {
    composable(RallyScreen.Overview.name) {
      Text(RallyScreen.Overview.name)
    }
    composable(RallyScreen.Accounts.name) {
        Text(RallyScreen.Accounts.name)
    }
    composable(RallyScreen.Bills.name) {
        Text(RallyScreen.Bills.name)
    }
}

NavHost peut désormais remplacer Box dans Scaffold. Transmettez Modifier à NavHost pour conserver innerPadding tel quel.

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        // FIXME: This duplicate source of truth
        //  will be removed later.
        var currentScreen by rememberSaveable {
            mutableStateOf(RallyScreen.Overview)
        }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen -> currentScreen = screen },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            NavHost(
                navController = navController,
                startDestination = RallyScreen.Overview.name,
                modifier = Modifier.padding(innerPadding)) {
            }
        }
    }
}

À ce stade, la barre supérieure n'est pas encore connectée. Par conséquent, si vous cliquez sur les onglets, le composable affiché n'est pas modifié. L'étape suivante vous permettra de résoudre ce problème.

Intégrer complètement les modifications de l'état de la barre de navigation

Dans cette étape, vous allez connecter RallyTabRow et supprimer le code de navigation manuel actuel. À la fin de cette étape, le composant de navigation se chargera entièrement du routage.

Toujours dans RallyActivity, vous pouvez constater que le composable RallyTabRow comporte un rappel appelé onTabSelected lorsqu'un utilisateur clique sur un onglet. Mettez à jour le code de sélection pour utiliser navController afin d'accéder à l'écran sélectionné.

C'est tout ce qu'il vous faut pour naviguer jusqu'à un écran via TabRow :

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        // FIXME: This duplicate source of truth
        //  will be removed later.
        var currentScreen by rememberSaveable {
            mutableStateOf(RallyScreen.Overview)
        }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen ->
                        navController.navigate(screen.name)
                },
                    currentScreen = currentScreen,
                )
            }

Avec cette modification, currentScreen ne sera plus mis à jour. Cela signifie que développer et réduire les éléments sélectionnés ne fonctionnera pas. Pour réactiver ce comportement, vous devez également mettre à jour la propriété currentScreen. Heureusement, Navigation conserve la pile "Retour" pour vous et peut vous fournir l'entrée actuelle de cette la pile sous la forme d'un State. Avec ce State, vous pouvez réagir aux modifications apportées à la pile "Retour". Vous pouvez même demander son itinéraire à l'entrée actuelle de la pile "Retour".

Pour terminer la migration de la sélection d'écran TabRow vers Navigation, mettez à jour currentScreen afin d'utiliser la pile "Retour" de Navigation comme ceci :

import androidx.navigation.compose.currentBackStackEntryAsState
...

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        val navController = rememberNavController()
        val backstackEntry = navController.currentBackStackEntryAsState()
        val currentScreen = RallyScreen.fromRoute(
            backstackEntry.value?.destination?.route
        )
        ...
    }
}

À ce stade, lorsque vous exécutez l'application, vous pouvez passer d'un écran à l'autre à l'aide des onglets, mais seul le nom de l'écran est affiché. Pour que l'écran puisse s'afficher, vous devez d'abord transférer RallyScreen vers Navigation.

Transférer RallyScreen vers Navigation

À la fin de cette étape, le composable sera totalement dissocié de l'énumération RallyScreen et déplacé vers NavHost. RallyScreen n'existera que pour fournir une icône et un titre à l'écran.

Ouvrez RallyScreen.kt. Déplacez l'implémentation de body de chaque écran vers les composables correspondants de votre NavHost dans RallyApp.

import com.example.compose.rally.data.UserData
import com.example.compose.rally.ui.accounts.AccountsBody
import com.example.compose.rally.ui.bills.BillsBody
import com.example.compose.rally.ui.overview.OverviewBody
...

NavHost(
    navController = navController,
    startDestination = Overview.name,
    modifier = Modifier.padding(innerPadding)
) {

    composable(Overview.name) {
        OverviewBody()
    }
    composable(Accounts.name) {
        AccountsBody(accounts = UserData.accounts)
    }
    composable(Bills.name) {
        BillsBody(bills = UserData.bills)
    }
}

À ce stade, vous pouvez supprimer sans crainte la fonction content, ainsi que le paramètre body et ses utilisations de RallyScreen. Le code est alors le suivant :

enum class RallyScreen(
    val icon: ImageVector,
) {
    Overview(
        icon = Icons.Filled.PieChart,
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
    );

    companion object {
        ...
    }
}

Exécutez à nouveau l'application. Les trois écrans initiaux s'affichent, et vous pouvez passer de l'un à l'autre via la barre d'onglets.

Activer les clics sur OverviewScreen

Dans cet atelier de programmation, les événements de clic sur OverviewBody ont été jusqu'à présent ignorés. Cela signifie qu'il était possible de cliquer sur le bouton "SEE ALL" (TOUT AFFICHER), mais qu'il ne se passait rien.

Enregistrement de l'écran "Overview" (Aperçu), défilement vers les destinations de clic finales et tentative de clic. Les clics ne fonctionnent pas, car ils ne sont pas encore implémentés.

Résolvons à présent ce problème.

OverviewBody peut accepter plusieurs fonctions en tant que rappels pour les événements de clic. Implémentons onClickSeeAllAccounts et onClickSeeAllBills pour naviguer jusqu'aux destinations appropriées.

Pour activer la navigation lorsque l'utilisateur clique sur le bouton "SEE ALL" (Tout afficher), utilisez navController et accédez à l'écran "Accounts" (Comptes) ou "Bills" (Factures). Ouvrez RallyActivity.kt, recherchez OverviewBody dans NavHost et ajoutez les appels de navigation.

OverviewBody(
    onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
    onClickSeeAllBills = { navController.navigate(Bills.name) },
)

Vous pouvez désormais modifier facilement le comportement des événements de clic pour OverviewBody. Conserver le navController au premier niveau de votre hiérarchie de navigation sans le transmettre directement à OverviewBody vous permet de prévisualiser ou de tester OverviewBody facilement et de manière isolée, sans avoir besoin d'un navController.

4. Naviguer avec des arguments

Ajoutons de nouvelles fonctionnalités à Rally. Nous allons ajouter un écran "Accounts" (Comptes) qui affiche les détails d'un compte lorsque l'utilisateur clique sur une ligne.

Un argument de navigation rend l'itinéraire dynamique. Les arguments de navigation sont des outils très puissants pour rendre les itinéraires dynamiques en transmettant un ou plusieurs arguments sur un itinéraire et en réglant les types d'arguments ou les valeurs par défaut.

Dans RallyActivity, ajoutez une nouvelle destination au graphique en ajoutant un nouveau composable dans le composant NavHost existant avec l'argument Accounts/{name}. Nous allons également spécifier une liste de navArgument pour cette destination. Nous allons définir un argument unique appelé "name", de type String.

import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.navArgument
...

val accountsName = RallyScreen.Accounts.name

composable(
    route = "$accountsName/{name}",
    arguments = listOf(
        navArgument("name") {
            // Make argument type safe
            type = NavType.StringType
        }
    )
) {
    // TODO
}

Le corps de chaque destination composable reçoit un paramètre (que nous n'avons pas encore utilisé) de l'objet NavBackStackEntry actuel qui modélise l'itinéraire et les arguments de la destination actuelle. Nous pouvons utiliser arguments pour récupérer l'argument (c'est-à-dire le nom du compte sélectionné), le rechercher dans UserData et le transmettre dans le composable SingleAccountBody.

Vous pourriez également indiquer une valeur par défaut à utiliser lorsque l'argument n'est pas fourni. Nous n'allons pas voir cela, car nous n'en avons pas besoin ici.

Le code devrait se présenter ainsi :

import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.navArgument
...

val accountsName = RallyScreen.Accounts.name
NavHost(...) {
    ...
    composable(
        "$accountsName/{name}",
        arguments = listOf(
            navArgument("name") {
                // Make argument type safe
                type = NavType.StringType
            }
        )
    ) { entry -> // Look up "name" in NavBackStackEntry's arguments
        val accountName = entry.arguments?.getString("name")
        // Find first name match in UserData
        val account = UserData.getAccount(accountName)
        // Pass account to SingleAccountBody
        SingleAccountBody(account = account)
    }
}

Maintenant que le composable est configuré avec l'argument, vous pouvez y accéder en utilisant navController comme suit : navController.navigate("${RallyScreen.Accounts.name}/$accountName").

Ajoutez cette fonction au paramètre onAccountClick de la déclaration de OverviewBody dans NavHost et au paramètre onAccountClick de AccountsBody.

Pour pouvoir réutiliser ces éléments, vous pouvez créer une fonction d'assistance privée, comme ci-dessous.

fun RallyNavHost(
    ...
) {
    NavHost(
        ...
    ) {
        composable(Overview.name) {
            OverviewBody(
                ...
                onAccountClick = { name ->
                    navigateToSingleAccount(navController, name)
                },
            )
        }
        composable(Accounts.name) {
            AccountsBody(accounts = UserData.accounts) { name ->
                navigateToSingleAccount(
                    navController = navController,
                    accountName = name
                )
            }
        }
        ...
    }
}

private fun navigateToSingleAccount(
    navController: NavHostController,
    accountName: String
) {
    navController.navigate("${Accounts.name}/$accountName")
}

À ce stade, lorsque vous exécutez l'application, vous pouvez cliquer sur chaque compte pour accéder à un écran contenant les données du compte concerné.

Enregistrement de l'écran "Overview" (Aperçu), défilement vers les destinations de clic finales et tentative de clic. Les clics mènent à présent à des destinations.

5. Activer les liens profonds

Outre des arguments, vous pouvez utiliser des liens profonds pour permettre à des applications tierces d'accéder à des destinations dans votre application. Dans cette section, vous allez ajouter un nouveau lien profond à l'itinéraire créé à l'étape précédente. Il sera ainsi possible d'accéder directement aux comptes dans votre application depuis des applications tierces via des liens profonds, à l'aide de leur nom.

Ajouter le filtre d'intent

Pour commencer, ajoutez le lien profond au fichier AndroidManifest.xml. Vous devez créer un filtre d'intent pour RallyActivity avec l'action VIEW et les catégories BROWSABLE et DEFAULT.

Ensuite, à l'aide du tag data, ajoutez un scheme, un host et un pathPrefix.

Dans cet atelier de programmation, nous utiliserons rally://accounts/{name} comme URL de lien profond.

Vous n'avez pas besoin de déclarer l'argument "name" dans le fichier AndroidManifest. Navigation l'analysera en tant qu'argument.

<activity
    android:name=".RallyActivity"
    android:windowSoftInputMode="adjustResize"
    android:label="@string/app_name"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="rally" android:host="accounts" />
    </intent-filter>
</activity>

Vous pouvez à présent réagir à l'intent entrant depuis RallyActivity.

Le composable que vous avez créé précédemment pour accepter les arguments peut aussi accepter le lien profond que vous venez de créer.

Ajoutez une liste de deepLinks à l'aide de la fonction navDeepLink. Transmettez uriPattern et fournissez l'URI correspondant au intent-filter ci-dessus. À l'aide du paramètre deepLinks, transmettez au composable le lien profond créé.

val accountsName = RallyScreen.Accounts.name

composable(
    "$accountsName/{name}",
    arguments = listOf(
        navArgument("name") {
            type = NavType.StringType
        },
    ),
    deepLinks =  listOf(navDeepLink {
        uriPattern = "rally://$accountsName/{name}"
    })
)

Votre application est maintenant prête à gérer les liens profonds. Pour tester son bon fonctionnement, installez la version actuelle de Rally sur un émulateur ou un appareil, ouvrez une ligne de commande et exécutez la commande suivante :

adb shell am start -d "rally://accounts/Checking" -a android.intent.action.VIEW

Vous accédez directement au compte courant. Cela fonctionne pour tous les noms de compte dans l'application.

6. Extraire le composant NavHost terminé

Votre NavHost est à présent terminé. Vous pouvez l'extraire depuis le composable RallyApp vers sa propre fonction et l'appeler RallyNavHost. C'est le seul et unique composable avec lequel vous devez utiliser directement navController. Même si vous ne créez pas navController dans RallyNavHost, vous pouvez l'utiliser pour sélectionner des onglets, qui font partie de la structure supérieure, dans RallyApp.

@Composable
fun RallyNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Overview.name,
        modifier = modifier
    ) {
        composable(Overview.name) {
            OverviewBody(
                onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
                onClickSeeAllBills = { navController.navigate(Bills.name) },
                onAccountClick = { name ->
                    navController.navigate("${Accounts.name}/$name")
                },
            )
        }
        composable(Accounts.name) {
            AccountsBody(accounts = UserData.accounts) { name ->
                navController.navigate("Accounts/${name}")
            }
        }
        composable(Bills.name) {
            BillsBody(bills = UserData.bills)
        }
        val accountsName = Accounts.name
        composable(
            "$accountsName/{name}",
            arguments = listOf(
                navArgument("name") {
                    type = NavType.StringType
                },
            ),
            deepLinks = listOf(navDeepLink {
                uriPattern = "example://rally/$accountsName/{name}"
            }),
        ) { entry ->
            val accountName = entry.arguments?.getString("name")
            val account = UserData.getAccount(accountName)
            SingleAccountBody(account = account)
        }
    }
}

Veillez également à remplacer le site d'appel d'origine par RallyNavHost(navController) afin que tout fonctionne correctement.

fun RallyApp() {
    RallyTheme {
    ...
        Scaffold(
        ...
        ) { innerPadding ->
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding)
            )

        }
     }
}

7. Tester Navigation dans Compose

Depuis le début de cet atelier de programmation, nous avons veillé à ne pas transmettre navController directement à des composables, mais à transmettre des rappels en tant que paramètres. Cela signifie que vous pouvez tester chaque composable séparément. Mais vous pouvez également tester l'intégralité de NavHost, et c'est ce que nous allons faire ici. Pour tester chaque fonction modulable séparément, consultez l'atelier de programmation Tests dans Jetpack Compose.

Préparer la classe de test

Votre NavHost peut être testé indépendamment de l'activité elle-même.

Comme ce test s'exécute sur un appareil Android, vous devez créer votre fichier de test dans le répertoire androidTest, sous /app/src/androidTest/java/com/example/compose/rally.

Créez ce fichier et appelez-le RallyNavHostTest.

Ensuite, pour utiliser les API de test de Compose, créez la règle de test Compose suivante.

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()

}

Vous êtes maintenant prêt à écrire un test réel.

Écrire votre premier test

Créez une fonction de test, qui doit être publique et annotée avec @Test. Dans cette fonction, vous devez définir le contenu à tester. Pour cela, utilisez le setContent de composeTestRule. Il utilise un paramètre composable et vous permet d'écrire du code Compose comme si vous vous trouviez dans une application standard. Configurez RallyNavHost comme vous l'avez fait dans RallyActivity.

import androidx.navigation.compose.rememberNavController
import org.junit.Assert.fail
import org.junit.Test
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: NavHostController

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
        fail()
    }
}

Si vous avez copié le code ci-dessus, l'appel fail() fait échouer votre test jusqu'à ce qu'une assertion réelle soit établie, ceci afin de vous rappeler de terminer le test.

Vous pouvez vérifier que l'écran approprié s'affiche à l'aide des descriptions de contenu. Dans cet atelier de programmation, les descriptions de contenu pour "Accounts Screen" et "Overview Screen" à utiliser pour valider le test vous sont fournies. Créez une propriété lateinit dans la classe de test elle-même afin de pouvoir l'utiliser également dans vos futurs tests.

Pour commencer, vérifiez que OverviewScreen s'affiche.

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.navigation.NavHostController
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: NavHostController

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

Supprimez l'appel fail(), puis réexécutez le test. Il réussit. Ouf !

Dans chacun des tests suivants, RallyNavHost sera configuré de la même manière. Vous pouvez donc l'extraire dans une fonction annotée avec @Before pour que votre code reste clair.

import org.junit.Before
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: NavHostController

    @Before
    fun setupRallyNavHost() {
        composeTestRule.setContent {
            navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
    }

    @Test
    fun rallyNavHost() {
        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

Vous pouvez tester l'implémentation de la navigation de différentes manières : en cliquant sur les éléments de l'UI qui doivent mener à une nouvelle destination ou en appelant navigate avec le nom d'itinéraire correspondant.

Tester via l'UI et la règle de test

Pour tester l'implémentation de votre application, il est préférable de tester les clics sur les éléments de l'UI. Rédigez un test pour cliquer sur le bouton "All Accounts" (Tous les comptes), qui vous redirige vers l'écran "Accounts" (Comptes), et vérifiez que l'écran affiché est le bon.

import androidx.compose.ui.test.performClick
...

@Test
fun rallyNavHost_navigateToAllAccounts_viaUI() {
    composeTestRule
        .onNodeWithContentDescription("All Accounts")
        .performClick()
    composeTestRule
        .onNodeWithContentDescription("Accounts Screen")
        .assertIsDisplayed()
}

Tester via l'interface utilisateur et navController

Vous pouvez également utiliser navController pour vérifier vos assertions. Pour cela, cliquez sur l'interface utilisateur, puis comparez l'itinéraire actuel à celui attendu en utilisant backstackEntry.value?.destination?.route.

import androidx.compose.ui.test.performScrollTo
import org.junit.Assert.assertEquals
...

@Test
fun rallyNavHost_navigateToBills_viaUI() {
    // When click on "All Bills"
    composeTestRule.onNodeWithContentDescription("All Bills").apply {
        performScrollTo()
        performClick()
    }
    // Then the route is "Bills"
    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "Bills")
}

Tester via navController

La troisième solution consiste à appeler directement navController.navigate. Attention cependant : les appels à navController.navigate doivent être effectués sur le thread UI. Pour cela, utilisez Coroutines avec le coordinateur de threads Main. Comme l'appel doit avoir lieu avant que vous puissiez émettre une assertion concernant un nouvel état, il doit être encapsulé dans un appel runBlocking.

runBlocking {
    withContext(Dispatchers.Main) {
        navController.navigate(RallyScreen.Accounts.name)
    }
}

Vous pouvez ainsi naviguer dans l'application et vous assurer que l'itinéraire vous emmène là où vous le souhaitez.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
...

@Test
fun rallyNavHost_navigateToAllAccounts_callingNavigate() {
    runBlocking {
        withContext(Dispatchers.Main) {
            navController.navigate(RallyScreen.Accounts.name)
        }
    }
    composeTestRule
        .onNodeWithContentDescription("Accounts Screen")
        .assertIsDisplayed()
}

Pour en savoir plus sur les tests dans Compose, consultez l'atelier de programmation mentionné dans la section "Et ensuite ?" à l'étape suivante.

8. Félicitations

Bravo ! Vous êtes arrivé au terme de cet atelier de programmation.

Vous venez d'ajouter Navigation à l'application Rally et avez découvert les concepts clés de la navigation dans Jetpack Compose. Vous avez appris à créer un graphique de navigation des destinations des composables, à ajouter des arguments aux itinéraires ainsi que des liens profonds, et à tester votre implémentation de différentes manières.

Et ensuite ?

Découvrez quelques-uns des ateliers de programmation...

Documents de référence