Naviguer entre les écrans avec Compose

Restez organisé à l'aide des collections Enregistrez et classez les contenus selon vos préférences.

1. Avant de commencer

Jusqu'à présent, les applications sur lesquelles vous avez travaillé n'utilisaient qu'un seul écran. Cependant, la majorité des applications que vous utilisez disposent probablement de plusieurs écrans entre lesquels vous pouvez naviguer. L'application Paramètres, par exemple, comporte de nombreuses pages de contenu réparties sur plusieurs écrans.

Première page de l'application Paramètres d'Android.

Page des paramètres après que l'utilisateur a sélectionné "Appareils connectés" sur la première page.

Page des paramètres après que l'utilisateur a sélectionné "Associer un nouvel appareil" sur la page précédente.

Dans Modern Android Development, les applications multiécrans sont créées à l'aide du composant Navigation de Jetpack. Le composant Navigation de Compose vous permet de créer facilement des applications multiécrans dans Compose à l'aide d'une approche déclarative, comme si vous créiez des interfaces utilisateur. Cet atelier de programmation présente les principes de base du composant Navigation de Compose. Il vous explique comment rendre la barre d'application responsive et comment envoyer des données de votre application vers une autre à l'aide d'intents, tout en présentant les bonnes pratiques à adopter dans une application de plus en plus complexe.

Conditions préalables

  • Vous maîtrisez le langage Kotlin, y compris les types de fonction, les lambdas et les fonctions de portée (scope).
  • Vous maîtrisez les mises en page Row et Column de base dans Compose.

Points abordés

  • Créer un composable NavHost pour définir des routes et des écrans dans votre application.
  • Naviguer entre les écrans à l'aide d'un NavHostController.
  • Manipuler la pile "Retour" pour revenir aux écrans précédents.
  • Utiliser des intents pour partager des données avec une autre application.
  • Personnaliser la barre d'application, y compris le titre et le bouton "Retour".

Objectifs de l'atelier

  • Vous allez implémenter la navigation dans une application multiécran.

Ce dont vous avez besoin

  • La dernière version d'Android Studio
  • Une connexion Internet pour télécharger le code de démarrage

2. Télécharger le code de démarrage

Pour commencer, téléchargez le code de démarrage :

Vous pouvez également cloner le dépôt GitHub pour obtenir le code :

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git

$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout starter

3. Tutoriel de l'application

L'application Cupcake est légèrement différente des applications avec lesquelles vous avez travaillé jusqu'à présent. Au lieu d'afficher tout le contenu sur un seul écran, cette application dispose en effet de quatre écrans distincts que l'utilisateur peut parcourir lorsqu'il commande des cupcakes.

Écran de démarrage de la commande

Le premier écran présente à l'utilisateur trois boutons correspondant à la quantité de cupcakes à commander.

Premier écran de l'application Cupcake proposant des options pour commander un, six ou douze cupcakes.

Dans le code, cela est représenté par le composable StartOrderScreen dans StartOrderScreen.kt.

L'écran se compose d'une seule colonne, contenant une image et du texte, ainsi que trois boutons personnalisés permettant de commander différentes quantités de cupcakes. Les boutons personnalisés sont implémentés par le composable SelectQuantityButton, qui se trouve également dans StartOrderScreen.kt.

Écran de sélection d'un parfum

Après avoir sélectionné la quantité de cupcakes, l'application invite l'utilisateur à choisir un parfum. L'application utilise ce que l'on appelle des cases d'option pour afficher les différentes options. Les utilisateurs peuvent faire leur choix parmi une liste de parfums.

Application Cupcake proposant à l'utilisateur différents parfums de cupcake.

La liste des parfums disponibles est stockée sous la forme d'une liste d'ID de ressources de chaîne dans data.DataSource.kt.

Écran de sélection de la date de retrait

Une fois que l'utilisateur a choisi un parfum, l'application lui propose une série de cases d'option pour lui permettre de sélectionner une date de retrait. Les options de retrait proviennent d'une liste renvoyée par la fonction pickupOptions() dans OrderViewModel.

Application Cupcake proposant à l'utilisateur plusieurs options pour la date de retrait.

L'écran Choose Flavor (Choisir un parfum) et Choose Pickup Date (Choisir une date de retrait) sont représentés par le même composable, SelectOptionScreen, dans SelectOptionScreen.kt. Pourquoi utiliser le même composable ? Ces écrans ont une mise en page parfaitement identique. La seule différence se situe au niveau des données, mais vous pouvez utiliser le même composable pour afficher les écrans de choix du parfum et de sélection de la date de retrait.

Écran du récapitulatif de la commande

Après avoir sélectionné la date de retrait, l'application affiche l'écran Order Summary (Récapitulatif de la commande) où l'utilisateur peut vérifier et finaliser sa commande.

Application Cupcake présentant le récapitulatif de la commande, y compris la quantité, le parfum, la date de retrait et le sous-total, en plus des options permettant d'envoyer la commande à une autre application ou d'annuler la commande.

Cet écran est implémenté par le composable OrderSummaryScreen dans OrderSummaryScreen.kt.

La mise en page comprend une Column contenant toutes les informations sur la commande, un composable Text pour le sous-total, ainsi que des boutons permettant d'envoyer la commande à une autre application ou d'annuler la commande et de revenir au premier écran.

Si les utilisateurs choisissent d'envoyer la commande à une autre application, l'application Cupcake affiche une bottom sheet présentant plusieurs options de partage.

Application Cupcake proposant à l'utilisateur des options de partage telles que SMS ou E-mail

L'état actuel de l'application est stocké dans data.OrderUiState.kt. La classe de données OrderUiState contient des propriétés permettant de stocker les sélections effectuées par l'utilisateur dans chaque écran.

Les écrans de l'application seront présentés dans le composable CupcakeApp. Toutefois, dans le projet de démarrage, l'application affiche simplement le premier écran. Pour le moment, il n'est pas possible de parcourir tous les écrans de l'application. Mais, ne vous inquiétez pas, c'est pour ça que nous sommes là ! Vous allez apprendre à définir des routes de navigation, à configurer un composable NavHost pour naviguer entre les écrans (également appelés destinations), à effectuer des intents pour intégrer des composants d'UI du système tels que l'écran de partage, et à faire en sorte que la barre d'application (AppBar) réponde aux changements de navigation.

Composables réutilisables

Le cas échéant, les applications exemples de ce cours sont conçues pour appliquer les bonnes pratiques. L'application Cupcake ne fait pas exception à la règle. Dans le package ui.components, vous trouverez un fichier nommé CommonUI.kt qui contient un composable FormattedPriceLabel. Plusieurs écrans de l'application utilisent ce composable pour appliquer une mise en forme cohérente au prix de la commande. Au lieu de dupliquer le même composable Text avec la même mise en forme et les mêmes modificateurs, vous pouvez définir FormattedPriceLabel une seule fois, puis le réutiliser autant de fois que nécessaire pour d'autres écrans.

Les écrans de choix du parfum et de sélection de la date de retrait utilisent le composable SelectOptionScreen, qui est également réutilisable. Ce composable accepte un paramètre options de type List<String> qui représente les options à afficher. Les options sont affichées dans un Row, constitué d'un composable RadioButton et d'un composable Text contenant chaque chaîne. Une Column entoure toute la mise en page et contient également un composable Text pour afficher le prix mis en forme, un bouton Annuler et un bouton Suivant.

4. Définir des routes et créer un NavHostController

Éléments du composant Navigation

Le composant Navigation se compose de trois éléments principaux :

  • NavController : permet de naviguer entre les destinations, c'est-à-dire entre les écrans de votre application.
  • NavGraph : mappe les destinations composables vers lesquelles il est possible de naviguer.
  • NavHost : composable faisant office de conteneur pour afficher la destination actuelle de NavGraph.

Dans cet atelier de programmation, vous allez vous concentrer sur NavController et NavHost. Dans NavHost, vous allez définir les destinations pour le NavGraph de l'application Cupcake.

Définir des routes pour les destinations de votre application

La route est l'un des concepts fondamentaux de la navigation dans une application Compose. Il s'agit d'une chaîne correspondant à une destination. Ce concept est semblable à celui de l'URL. Tout comme une URL pointe vers une page spécifique d'un site Web, une route est une chaîne qui correspond à une destination et sert d'identifiant unique. Une destination est généralement un composable unique ou un groupe de composables correspondant à ce que voit l'utilisateur. L'application Cupcake a besoin de destinations pour les écrans de démarrage de la commande, de choix du parfum, de sélection de la date de retrait et de récapitulatif de la commande.

Le nombre d'écrans dans une application est limité. Par conséquent, le nombre de routes l'est également. Vous pouvez définir les routes d'une application à l'aide d'une classe d'énumération. En langage Kotlin, les classes d'énumération ont une propriété de nom qui renvoie une chaîne avec le nom de la propriété.

Vous allez commencer par définir les quatre routes de l'application Cupcake.

  • Start : sélectionnez la quantité de cupcakes à l'aide de l'un des trois boutons.
  • Flavor : sélectionnez le parfum parmi les choix proposés.
  • Pickup : sélectionnez la date de retrait dans la liste.
  • Summary : vérifiez les sélections effectuées, puis envoyez ou annulez la commande.

Ajoutez une classe d'énumération pour définir les routes.

  1. Dans CupcakeScreen.kt, au-dessus du composable CupcakeAppBar, ajoutez une classe d'énumération nommée CupcakeScreen.
enum class CupcakeScreen() {

}
  1. Ajoutez quatre cas à la classe d'énumération : Start, Flavor, Pickup et Summary.
enum class CupcakeScreen() {
    Start,
    Flavor,
    Pickup,
    Summary
}

Ajouter un NavHost à votre application

Un NavHost est un composable qui affiche d'autres destinations composables, en fonction d'une route donnée. Par exemple, si la route est Flavor, NavHost affiche l'écran permettant de sélectionner le parfum du cupcake. Si la route est Summary, l'application affiche l'écran récapitulatif.

La syntaxe de NavHost est la même que pour tout autre composable.

fae7688d6dd53de9.png

Il existe deux paramètres importants.

  • navController : instance de la classe NavHostController. Vous pouvez utiliser cet objet pour naviguer entre les écrans, par exemple en appelant la méthode navigate() pour accéder à une autre destination. Vous pouvez obtenir NavHostController en appelant rememberNavController() à partir d'une fonction modulable.
  • startDestination : route de chaîne qui définit la destination affichée par défaut la première fois que l'application affiche le NavHost. Dans le cas de l'application Cupcake, il doit s'agir de la route Start.

Comme les autres composables, NavHost utilise également un paramètre modifier.

Vous allez ajouter un NavHost au composable CupcakeApp dans CupcakeScreen.kt. Tout d'abord, vous avez besoin d'une référence au contrôleur de navigation. Vous pouvez utiliser le contrôleur de navigation situé dans l'élément NavHost que vous ajoutez maintenant et dans l'élément AppBar que vous ajouterez ultérieurement. Par conséquent, vous devez déclarer la variable dans le composable CupcakeApp().

  1. Ouvrez CupcakeScreen.kt.
  2. Au-dessus de la variable viewModel dans le composable CupcakeApp, créez une variable à l'aide d'un val nommé navController et définissez-la sur le résultat de l'appel de rememberNavController().
@Composable
fun CupcakeApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()

    ...
}
  1. Dans Scaffold, sous la variable uiState, ajoutez un composable NavHost.
Scaffold(
    ...
) { innerPadding ->
    val uiState by viewModel.uiState.collectAsState()

    NavHost()
}
  1. Transmettez la variable navController pour le paramètre navController et CupcakeScreen.Start.name pour le paramètre startDestination. Transmettez le modificateur qui a été transmis à CupcakeApp() pour le paramètre de modificateur.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
}

Gérer les routes dans votre NavHost

Comme les autres composables, NavHost utilise un type de fonction pour son contenu.

f67974b7fb3f0377.png

Dans la fonction de contenu d'un NavHostController, appelez la fonction composable(). La fonction composable() comporte deux paramètres obligatoires.

  • route : chaîne correspondant au nom d'une route. Il peut s'agir de n'importe quelle chaîne unique. Vous utiliserez la propriété de nom des constantes de l'énumération CupcakeScreen.
  • content : ici, vous pouvez appeler un composable que vous souhaitez afficher pour la route fournie.

Vous appellerez la fonction composable() une fois pour chacune des quatre routes.

  1. Appelez la fonction composable() en transmettant CupcakeScreen.Start.name pour la route.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {

    }
}
  1. Dans le lambda de fin, appelez le composable StartOrderScreen, en transmettant quantityOptions pour la propriété quantityOptions.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {
        StartOrderScreen(
            quantityOptions = quantityOptions
        )
    }
}
  1. Sous le premier appel vers composable(), appelez à nouveau composable() en transmettant CupcakeScreen.Flavor.name pour route.
composable(route = CupcakeScreen.Flavor.name) {

}
  1. Dans le lambda de fin, obtenez une référence à LocalContext.current et stockez-la dans une variable nommée context. Vous pouvez utiliser cette variable pour obtenir les chaînes de la liste des ID de ressources du modèle de vue afin d'afficher la liste des parfums.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current

}
  1. Appelez le composable SelectOptionScreen.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(

    )
}
  1. L'écran de choix du parfum doit afficher et mettre à jour le sous-total lorsque l'utilisateur sélectionne un parfum. Transmettez uiState.price pour le paramètre subtotal.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price
    )
}
  1. L'écran de choix du parfum affiche la liste des parfums provenant des ressources de chaîne de l'application. Créez une liste de chaînes à partir de la liste des parfums dans le modèle de vue. Vous pouvez transformer la liste d'ID de ressources en une liste de chaînes en utilisant la fonction map() et en appelant stringResource().
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> stringResource(id) }
    )
}
  1. Pour le paramètre onSelectionChanged, transmettez une expression lambda qui appelle setFlavor() sur le modèle de vue, en transmettant it (l'argument transmis à onSelectionChanged()).
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}

L'écran de sélection de la date de retrait est semblable à celui du choix du parfum. La seule différence réside dans les données transmises au composable SelectOptionScreen.

  1. Appelez à nouveau la fonction composable(), en transmettant CupcakeScreen.Pickup.name pour le paramètre route.
composable(route = CupcakeScreen.Pickup.name) {

}
  1. Dans le lambda de fin, appelez le composable SelectOptionScreen et transmettez uiState.price pour le subtotal, comme précédemment. Transmettez uiState.pickupOptions pour le paramètre options et une expression lambda qui appelle setDate() sur le viewModel pour le paramètre onSelectionChanged.
SelectOptionScreen(
    subtotal = uiState.price,
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  1. Appelez une nouvelle fois composable(), en transmettant CupcakeScreen.Summary.name pour route.
composable(route = CupcakeScreen.Summary.name) {

}
  1. Dans le lambda de fin, appelez le composable OrderSummaryScreen(), en transmettant la variable uiState pour le paramètre orderUiState.
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState
    )
}

La configuration de NavHost est maintenant terminée. Dans la section suivante, vous allez faire en sorte que votre application change de route et bascule entre les écrans lorsque l'utilisateur appuie sur chacun des boutons.

5. Naviguer entre les routes

Maintenant que vous avez défini vos routes et les avez mappées aux composables d'un NavHost, il est temps de passer d'un écran à l'autre. NavHostController (la propriété navController provenant de l'appel de rememberNavController()) est responsable de la navigation entre les routes. Notez toutefois que cette propriété est définie dans le composable CupcakeApp. Vous avez besoin d'une méthode permettant d'y accéder à partir des différents écrans de votre application.

Simple, n'est-ce pas ? Transmettez simplement navController en tant que paramètre à chacun des composables.

Certes cette méthode fonctionne, mais elle n'est pas idéale pour concevoir votre application. L'un des avantages de l'utilisation de NavHost pour gérer la navigation dans votre application est que la logique de navigation est séparée de l'UI individuelle. Cette option permet d'éviter certains inconvénients majeurs liés à la transmission de navController en tant que paramètre.

  • La logique de navigation est conservée à un seul endroit. Cela facilite la maintenance de votre code et empêche l'apparition de bugs causés par une navigation accidentelle vers des écrans individuels dans votre application.
  • Dans les applications qui doivent fonctionner sur différents facteurs de forme (téléphones en mode Portrait, téléphones pliables ou tablettes équipées d'un grand écran, par exemple), un bouton peut déclencher ou non la navigation en fonction de la mise en page de l'application. Chaque écran de l'application doit être indépendant.

Notre approche consiste à transmettre un type de fonction à chaque composable pour savoir ce qui doit se passer lorsqu'un utilisateur clique sur le bouton. De cette façon, le composable et ses composables enfants détermineront à quel moment appeler la fonction. Cependant, la logique de navigation n'est pas exposée sur chaque écran de l'application. Tout le comportement de navigation est géré dans le NavHost.

Ajouter des gestionnaires de bouton à StartOrderScreen

Pour commencer, vous allez ajouter un paramètre de type de fonction qui est appelé lorsque l'utilisateur appuie sur l'un des boutons de quantité du premier écran. Cette fonction est transmise au composable StartOrderScreen, et est chargée de mettre à jour le modèle de vue et d'accéder à l'écran suivant.

  1. Ouvrez StartOrderScreen.kt.
  2. Sous le paramètre quantityOptions, et avant le paramètre de modificateur, ajoutez un paramètre nommé onNextButtonClicked, de type () -> Unit.
@Composable
fun StartOrderScreen(
    quantityOptions: List<Pair<Int, Int>>,
    onNextButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
...
}

Chaque bouton correspond à une quantité différente de cupcakes. Vous aurez besoin de ces informations pour que la fonction transmise pour onNextButtonClicked puisse mettre à jour le modèle de vue en conséquence.

  1. Modifiez le type du paramètre onNextButtonClicked pour qu'il accepte un paramètre Int.
onNextButtonClicked: (Int) -> Unit,

Pour que Int soit transmis lors de l'appel de onNextButtonClicked(), examinez le type du paramètre quantityOptions.

Le type est List<Pair<Int, Int>> ou une liste de Pair<Int, Int>. Vous ne connaissez peut-être pas le type Pair mais, comme son nom l'indique, il s'agit d'une paire de valeurs. Pair accepte deux paramètres de type générique. Dans le cas présent, ils sont tous les deux de type Int.

f106c791f1e8ae19.png

C'est soit la première propriété soit la deuxième qui accède à chaque élément d'une paire. Dans le cas du paramètre quantityOptions du composable StartOrderScreen, le premier Int est un ID de ressource pour la chaîne à afficher sur chaque bouton. Le deuxième Int correspond à la quantité réelle de cupcakes.

Nous transmettrons la deuxième propriété de la paire sélectionnée lors de l'appel de la fonction onNextButtonClicked().

  1. Transmettez une expression lambda pour le paramètre onClick de SelectQuantityButton.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = {  }
    )
}
  1. Dans l'expression lambda, appelez onNextButtonClicked en transmettant item.second, soit le nombre de cupcakes.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = { onNextButtonClicked(item.second) }
    )
}

Ajouter des gestionnaires de bouton à SelectOptionScreen

  1. Sous le paramètre onSelectionChanged du composable SelectOptionScreen dans SelectOptionScreen.kt, ajoutez un paramètre nommé onCancelButtonClicked, de type () -> Unit.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Sous le paramètre onCancelButtonClicked, ajoutez un autre paramètre de type () -> Unit nommé onNextButtonClicked.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    onNextButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Transmettez onCancelButtonClicked pour le paramètre onClick du bouton "Cancel" (Annuler).
OutlinedButton(modifier = Modifier.weight(1f), onClick = onCancelButtonClicked) {
    Text(stringResource(R.string.cancel))
}
  1. Transmettez onNextButtonClicked pour le paramètre onClick du bouton "Next" (Suivant).
Button(
    modifier = Modifier.weight(1f),
    enabled = selectedValue.isNotEmpty(),
    onClick = onNextButtonClicked
) {
    Text(stringResource(R.string.next))
}

Ajouter des gestionnaires de bouton à SummaryScreen

Pour terminer, ajoutez des fonctions de gestionnaire de bouton pour les boutons Cancel (Annuler) et Send (Envoyer) sur l'écran récapitulatif.

  1. Dans le composable OrderSummaryScreen de OrderSummaryScreen.kt, ajoutez un paramètre nommé onCancelButtonClicked, de type () -> Unit.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Ajoutez un autre paramètre de type () -> Unit et nommez-le onSendButtonClicked.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    onSendButtonClicked: (String, String) -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Transmettez onSendButtonClicked pour le paramètre onClick du bouton Send (Envoyer). Transmettez newOrder et orderSummary, les deux variables définies précédemment dans OrderSummaryScreen. Ces chaînes sont constituées des données réelles que l'utilisateur peut partager avec une autre application.
Button(
    modifier = Modifier.fillMaxWidth(),
    onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
    Text(stringResource(R.string.send))
}
  1. Transmettez onCancelButtonClicked pour le paramètre onClick du bouton Cancel (Annuler).
OutlinedButton(
    modifier = Modifier.fillMaxWidth(),
    onClick = onCancelButtonClicked
) {
    Text(stringResource(R.string.cancel))
}

Pour accéder à une autre route, appelez simplement la méthode navigate() sur votre instance de NavHostController.

fc8aae3911a6a25d.png

La méthode de navigation accepte un seul paramètre : une chaîne correspondant à une route définie dans votre NavHost. Si la route correspond à l'un des appels vers composable() dans le NavHost, l'application accède à cet écran.

Vous allez transmettre des fonctions qui appellent navigate() lorsque l'utilisateur appuie sur des boutons des écrans Start, Flavor et Pickup.

  1. Dans CupcakeScreen.kt, recherchez l'appel vers composable() pour l'écran de démarrage. Pour le paramètre onNextButtonClicked, transmettez une expression lambda.
StartOrderScreen(
    quantityOptions = quantityOptions,
    onNextButtonClicked = {
    }
)

Vous vous souvenez de la propriété Int transmise à cette fonction pour le nombre de cupcakes ? Avant de passer à l'écran suivant, vous devez mettre à jour le modèle de vue afin que l'application affiche le sous-total correct.

  1. Appelez setQuantity sur viewModel, en transmettant it.
onNextButtonClicked = {
    viewModel.setQuantity(it)
}
  1. Appelez navigate() sur navController, en transmettant CupcakeScreen.Flavor.name pour la route.
onNextButtonClicked = {
    viewModel.setQuantity(it)
    navController.navigate(CupcakeScreen.Flavor.name)
}
  1. Pour le paramètre onNextButtonClicked sur l'écran de choix du parfum, il suffit de transmettre un lambda qui appelle navigate(), en transmettant CupcakeScreen.Pickup.name pour route.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = {
            navController.navigate(CupcakeScreen.Pickup.name) },
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}
  1. Transmettez un lambda vide pour onCancelButtonClicked, que vous implémenterez par la suite.
SelectOptionScreen(
     subtotal = uiState.price,
    onNextButtonClicked = {
        navController.navigate(CupcakeScreen.Pickup.name) },
    onCancelButtonClicked = {},
    options = flavors.map { id -> context.resources.getString(id) },
    onSelectionChanged = { viewModel.setFlavor(it) }
)
  1. Pour le paramètre onNextButtonClicked sur l'écran de sélection de la date de retrait, transmettez un lambda qui appelle navigate(), en transmettant CupcakeScreen.Summary.name pour route.
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = {
            navController.navigate(CupcakeScreen.Summary.name)
        },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) }
    )
}
  1. Transmettez à nouveau un lambda vide pour onCancelButtonClicked().
SelectOptionScreen(
    subtotal = uiState.price,
    onNextButtonClicked = {
        navController.navigate(CupcakeScreen.Summary.name) },
    onCancelButtonClicked = {},
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  1. Pour OrderSummaryScreen, transmettez des lambdas vides pour onCancelButtonClicked et onSendButtonClicked. Ajoutez des paramètres pour le subject et le summary qui sont transmis à onSendButtonClicked (que vous implémenterez bientôt).
composable(route = CupcakeScreen.Summary.name) {
   val context = LocalContext.current
   OrderSummaryScreen(
       orderUiState = uiState,
       onCancelButtonClicked = {},
       onSendButtonClicked = { subject: String, summary: String ->

       }
   )
}

Vous devriez maintenant être en mesure de parcourir chaque écran de votre application. Notez que l'appel de navigate() modifie non seulement l'écran, mais le place également en haut de la pile "Retour". De même, lorsque vous appuyez sur le bouton "Retour" du système, vous pouvez revenir à l'écran précédent.

L'application empile chaque écran au-dessus du précédent et le bouton Retour (bade5f3ecb71e4a2.png) peut les supprimer. L'historique des écrans, depuis la startDestination en bas jusqu'à l'écran supérieur qui vient d'être affiché, est appelé pile "Retour".

Accéder à l'écran de démarrage

Contrairement au bouton "Retour" du système, le bouton Cancel (Annuler) ne permet pas de revenir à l'écran précédent. Au lieu de cela, il doit supprimer tous les écrans de la pile "Retour" et revenir à l'écran de démarrage.

Pour ce faire, appelez la méthode popBackStack().

2f382e5eb319b4b8.png

La méthode popBackStack() a deux paramètres obligatoires.

  • route : chaîne représentant la route de la destination à laquelle vous souhaitez revenir.
  • inclusive : valeur booléenne qui, si elle est définie sur "true", supprime également la route spécifiée. Si la valeur est définie sur "false", popBackStack() supprime toutes les destinations au-dessus de la destination de départ, à l'exclusion de celle-ci, ce qui en fait l'écran principal visible par l'utilisateur.

Lorsque l'utilisateur appuie sur le bouton Cancel (Annuler) de l'un des écrans, l'application réinitialise l'état du modèle de vue et appelle popBackStack(). Vous devez d'abord implémenter une méthode pour le faire, puis la transmettre pour le paramètre approprié sur les trois écrans dotés de boutons Cancel (Annuler).

  1. Après la fonction CupcakeApp(), définissez une fonction privée appelée cancelOrderAndNavigateToStart().
private fun cancelOrderAndNavigateToStart() {
}
  1. Ajoutez deux paramètres : viewModel de type OrderViewModel et navController de type NavHostController.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
}
  1. Dans le corps de la fonction, appelez resetOrder() sur viewModel.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
}
  1. Appelez popBackStack() sur navController, en transmettant CupcakeScreen.Start.name pour route et false pour inclusive.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
    navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
  1. Dans le composable CupcakeApp(), transmettez cancelOrderAndNavigateToStart pour les paramètres onCancelButtonClicked des deux composables SelectOptionScreen et du composable OrderSummaryScreen.
composable(route = CupcakeScreen.Start.name) {
   StartOrderScreen(
       quantityOptions = quantityOptions,
       onNextButtonClicked = {
           viewModel.setQuantity(it)
           navController.navigate(CupcakeScreen.Flavor.name)
       }
   )
}
composable(route = CupcakeScreen.Flavor.name) {
   val context = LocalContext.current
   SelectOptionScreen(
       subtotal = uiState.price,
       onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
       onCancelButtonClicked = {
           cancelOrderAndNavigateToStart(viewModel, navController)
       },
       options = flavors.map { id -> context.resources.getString(id) },
       onSelectionChanged = { viewModel.setFlavor(it) }
   )
}
  1. Exécutez votre application et vérifiez que le bouton Cancel (Annuler) de n'importe quel écran fait revenir l'utilisateur au premier écran.

6. Accéder à une autre application

Jusqu'à présent, vous avez appris à accéder à un autre écran de votre application et à revenir à l'écran racine. Il ne vous reste qu'une seule étape pour implémenter la navigation dans l'application Cupcake. L'écran de récapitulatif de la commande permet à l'utilisateur d'envoyer sa commande vers une autre application. Cette option ouvre une bottom sheet (un composant de l'interface utilisateur qui couvre la partie inférieure de l'écran) qui affiche les options de partage.

Cet élément de l'UI ne fait pas partie de l'application Cupcake. En fait, il est fourni par le système d'exploitation Android. L'UI du système, telle que l'écran de partage, n'est pas appelée par votre navController. Vous devez utiliser ce que l'on appelle un intent.

Un intent est une requête adressée au système pour qu'il effectue une action ; il s'agit généralement de présenter une nouvelle activité. Il existe de nombreux intents différents. Nous vous invitons à consulter la documentation pour obtenir la liste complète. Dans le cas présent, nous nous intéresserons à l'intent appelé ACTION_SEND. Vous pouvez fournir certaines données à cet intent (comme une chaîne) et présenter des actions de partage appropriées.

Le processus de configuration de base d'un intent est le suivant :

  1. Créez un objet d'intent et spécifiez l'intent. Par exemple, ACTION_SEND.
  2. Spécifiez le type de données supplémentaires envoyées avec l'intent. Vous pouvez utiliser "text/plain" pour un texte simple, mais d'autres types, tels que <insert examples>, sont également disponibles.
  3. Le cas échéant, transmettez des données supplémentaires à l'intent, comme le texte ou l'image à partager, en appelant la méthode putExtra(). Cet intent utilise deux extras : EXTRA_SUBJECT et EXTRA_TEXT.
  4. Appelez la méthode de contexte startActivity() en transmettant une activité créée à partir de l'intent.

Nous allons vous expliquer comment créer un intent d'action de partage. Notez cependant que le processus est le même pour les autres types d'intents. Pour vos futurs projets, nous vous invitons à consulter la documentation correspondant au type de données spécifique et aux extras dont vous avez besoin.

Pour créer un intent d'envoi de commande de cupcakes vers une autre application, procédez comme suit :

  1. Dans CupcakeScreen.kt, sous le composable CupcakeApp, créez une fonction privée nommée shareOrder().
private fun shareOrder()
  1. Ajoutez un paramètre nommé context, de type Context.
private fun shareOrder(context: Context) {
}
  1. Ajoutez deux paramètres String : subject et summary. Ces chaînes seront affichées sur la feuille d'action de partage.
private fun shareOrder(context: Context, subject: String, summary: String) {
}
  1. Dans le corps de la fonction, créez un intent nommé intent et transmettez Intent.ACTION_SEND en tant qu'argument.
val intent = Intent(Intent.ACTION_SEND)

Puisque cet objet Intent ne doit être configuré qu'une seule fois, vous pouvez rendre les prochaines lignes de code plus compactes en utilisant la fonction apply() étudiée au cours d'un atelier de programmation précédent.

  1. Appelez apply() sur le nouvel intent et transmettez une expression lambda.
val intent = Intent(Intent.ACTION_SEND).apply {

}
  1. Dans le corps du lambda, définissez le type sur "text/plain". Étant donné que vous effectuez cette opération dans une fonction transmise à apply(), il n'est pas nécessaire de faire référence à l'identifiant de l'objet, intent.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
}
  1. Appelez putExtra(), en transmettant l'objet pour EXTRA_SUBJECT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
}
  1. Appelez putExtra(), en transmettant le récapitulatif pour EXTRA_TEXT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
    putExtra(Intent.EXTRA_TEXT, summary)
}
  1. Appelez la méthode de contexte startActivity().
context.startActivity(

)
  1. Dans le lambda transmis à startActivity(), créez une activité à partir de l'intent en appelant la méthode de classe createChooser(). Transmettez l'intent pour le premier argument et la ressource de chaîne new_cupcake_order.
context.startActivity(
    Intent.createChooser(
        intent,
        context.getString(R.string.new_cupcake_order)
    )
)
  1. Dans le composable CupcakeApp, lors de l'appel vers composable() pour CucpakeScreen.Summary.name, obtenez une référence à l'objet de contexte afin de pouvoir le transmettre à la fonction shareOrder().
composable(route = CupcakeScreen.Summary.name) {
    val context = LocalContext.current

    ...
}
  1. Dans le corps du lambda de onSendButtonClicked(), appelez shareOrder() en transmettant les arguments context, subject et summary.
onSendButtonClicked = { subject: String, summary: String ->
    shareOrder(context, subject = subject, summary = summary)
}
  1. Exécutez votre application et parcourez les écrans.

Lorsque vous cliquez sur Send Order to Another App (Envoyer la commande à une autre application), des actions de partage, telles que Messaging (Messagerie) et Bluetooth, doivent normalement s'afficher sur la bottom sheet, avec l'objet et le récapitulatif que vous avez fournis en tant qu'extras.

Application Cupcake proposant à l'utilisateur des options de partage telles que SMS ou E-mail

7. Faire en sorte que la barre d'application réponde à la navigation

Même si votre application fonctionne et permet de naviguer entre les différents écrans, il manque encore quelque chose sur les captures d'écran présentées au début de cet atelier de programmation. La barre d'application (AppBar) ne réagit pas automatiquement à la navigation. Le titre n'est pas mis à jour lorsque l'application accède à une nouvelle route et, le cas échéant, le bouton "Niveau supérieur" n'apparaît pas avant le titre.

Le code de démarrage contient un composable qui gère l'AppBar, nommé CupcakeAppBar. Maintenant que vous avez implémenté la navigation dans l'application, vous pouvez utiliser les informations de la pile "Retour" pour afficher le titre correct et, si nécessaire, le bouton "Niveau supérieur".

Le bouton "Niveau supérieur" ne s'affiche que si la pile "Retour" contient un composable. Si l'application ne comporte aucun écran dans la pile "Retour" (StartOrderScreen est alors affiché), le bouton "Niveau supérieur" n'est pas affiché. Pour le vérifier, vous avez besoin d'une référence à la pile "Retour".

  1. Dans le composable CupcakeApp, sous la variable navController, créez une variable nommée backStackEntry et appelez la méthode currentBackStackEntry() de navController à l'aide du délégué by.
@Composable
fun CupcakeApp(modifier: Modifier = Modifier, viewModel: OrderViewModel = viewModel()){

    val navController = rememberNavController()

    val backStackEntry by navController.currentBackStackEntryAsState()

    ...
}
  1. Dans CupcakeAppBar, transmettez backStackEntry?.destination?.route pour le paramètre currentScreen. Comme il peut s'agir d'une valeur nulle, utilisez l'opérateur Elvis (:?) pour spécifier CupcakeScreen.Start.name comme valeur par défaut.
currentScreen = backStackEntry?.destination?.route ?: CupcakeScreen.Start.name,

Tant qu'il y a un écran derrière l'écran actuel dans la pile "Retour", le bouton "Niveau supérieur" doit normalement s'afficher. Vous pouvez utiliser une expression booléenne pour déterminer si le bouton "Niveau supérieur" doit être affiché.

  1. Pour le paramètre canNavigateBack, transmettez une expression booléenne qui vérifie si la propriété previousBackStackEntry de navController n'est pas égale à la valeur "null".
canNavigateBack = navController.previousBackStackEntry != null,
  1. Pour revenir à l'écran précédent, appelez la méthode navigateUp() de navController.
navigateUp = { navController.navigateUp() }
  1. Exécutez votre application.

Notez que le titre AppBar est maintenant mis à jour pour tenir compte de l'écran actuel. Lorsque vous accédez à un écran autre que StartOrderScreen, le bouton "Retour" doit s'afficher pour vous permettre de revenir à l'écran précédent.

Animation illustrant la navigation entre tous les écrans de l'application Cupcake terminée.

8. Télécharger le code de solution

Pour télécharger le code de l'atelier de programmation terminé, utilisez la commande git suivante :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git

$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout navigation

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous le souhaitez, vous pouvez consulter le code de solution de cet atelier de programmation sur GitHub.

9. Résumé

Félicitations ! Vous avez utilisé, avec succès, le composant Navigation de Jetpack pour passer d'une simple application monopage à une application multiécran complexe pour parcourir plusieurs écrans. Vous avez défini des routes, vous les avez traitées dans un NavHost et vous avez utilisé des paramètres de type de fonction pour séparer la logique de navigation des différents écrans. Vous avez également appris à envoyer des données vers une autre application à l'aide d'intents et à personnaliser la barre d'application en réponse à la navigation. Dans les prochains modules, vous continuerez à utiliser ces compétences en travaillant sur d'autres applications multiécrans de plus en plus complexes.

En savoir plus