Naviguer entre les écrans avec Compose

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.

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 du 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

Si vous le souhaitez, vous pouvez consulter le code de démarrage de cet atelier de programmation sur GitHub.

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. Si vous exécutez l'application, vous ne verrez rien et vous ne pourrez pas naviguer entre ces écrans, car le composant de navigation n'a pas encore été ajouté au code de l'application. Toutefois, vous pouvez toujours vérifier les aperçus composables pour chaque écran et les faire correspondre aux écrans d'application finaux ci-dessous.

Écran de démarrage de la commande

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

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.

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.

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.

Cet écran est implémenté par le composable OrderSummaryScreen dans SummaryScreen.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 Android ShareSheet qui présente plusieurs options de partage.

a32e016a6ccbf427.png

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. Dans Scaffold, sous la variable uiState, ajoutez un composable NavHost.
import androidx.navigation.compose.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. Transmettez un lambda de fin vide pour le paramètre final.
import androidx.compose.foundation.layout.padding

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 NavHost, 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.
import androidx.navigation.compose.composable

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. Pour la transmission de modifier dans Modifier.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium)).
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.res.dimensionResource
import com.example.cupcake.ui.StartOrderScreen
import com.example.cupcake.data.DataSource

NavHost(
    navController = navController,
    startDestination = CupcakeScreen.Start.name,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {
        StartOrderScreen(
            quantityOptions = DataSource.quantityOptions,
            modifier = Modifier
                .fillMaxSize()
                .padding(dimensionResource(R.dimen.padding_medium))
        )
    }
}
  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. Context est une classe abstraite dont l'implémentation est fournie par le système Android. Elle permet l'accès à des ressources et à des classes propres à l'application, ainsi qu'à des appels pour effectuer des opérations au niveau de l'application, comme les activités de lancement par exemple. 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 saveurs.
import androidx.compose.ui.platform.LocalContext

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. Transformez la liste d'ID de ressources en une liste de chaînes en utilisant la fonction map() et en appelant context.resources.getString(id) pour chaque saveur.
import com.example.cupcake.ui.SelectOptionScreen

composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = DataSource.flavors.map { id -> context.resources.getString(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()). Pour le paramètre modifier, transmettez Modifier.fillMaxHeight()..
import androidx.compose.foundation.layout.fillMaxHeight
import com.example.cupcake.data.DataSource.flavors

composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}

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. Pour le paramètre modifier, transmettez-le dans Modifier.fillMaxHeight()..
SelectOptionScreen(
    subtotal = uiState.price,
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) },
    modifier = Modifier.fillMaxHeight()
)
  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. Pour le paramètre modifier, transmettez-le dans Modifier.fillMaxHeight()..
import com.example.cupcake.ui.OrderSummaryScreen

composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState,
        modifier = Modifier.fillMaxHeight()
    )
}

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
){
    ...
}
  1. Maintenant que le composable StartOrderScreen attend une valeur pour onNextButtonClicked, recherchez StartOrderPreview et transmettez un corps de lambda vide au paramètre onNextButtonClicked.
@Preview
@Composable
fun StartOrderPreview() {
    CupcakeTheme {
        StartOrderScreen(
            quantityOptions = DataSource.quantityOptions,
            onNextButtonClicked = {},
            modifier = Modifier
                .fillMaxSize()
                .padding(dimensionResource(R.dimen.padding_medium))
        )
    }
}

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.

8326701a77706258.png

Il s'agit soit de la première propriété soit de 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. Recherchez l'expression lambda vide pour le paramètre onClick du 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 de SelectOptionScreen.kt, ajoutez un paramètre nommé onCancelButtonClicked de type () -> Unit avec une valeur par défaut de {}.
@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, avec une valeur par défaut de {}.
@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 SummaryScreen.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 (String, String) -> Unit et nommez-le onSendButtonClicked.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    onSendButtonClicked: (String, String) -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Le composable OrderSummaryScreen attend désormais des valeurs pour onSendButtonClicked et onCancelButtonClicked. Recherchez OrderSummaryPreview, transmettez un corps de lambda vide avec deux paramètres String à onSendButtonClicked et un corps lambda vide aux paramètres onCancelButtonClicked.
@Preview
@Composable
fun OrderSummaryPreview() {
   CupcakeTheme {
       OrderSummaryScreen(
           orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
           onSendButtonClicked = { subject: String, summary: String -> },
           onCancelButtonClicked = {},
           modifier = Modifier.fillMaxHeight()
       )
   }
}
  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 String 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 = DataSource.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 = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
  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 = DataSource.flavors.map { id -> context.resources.getString(id) },
    onSelectionChanged = { viewModel.setFlavor(it) },
    modifier = Modifier.fillMaxHeight()
)
  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) },
        modifier = Modifier.fillMaxHeight()
    )
}
  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) },
    modifier = Modifier.fillMaxHeight()
)
  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) {
    OrderSummaryScreen(
        orderUiState = uiState,
        onCancelButtonClicked = {},
        onSendButtonClicked = { subject: String, summary: String ->

        },
        modifier = Modifier.fillMaxHeight()
    )
}

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 = DataSource.quantityOptions,
        onNextButtonClicked = {
            viewModel.setQuantity(it)
            navController.navigate(CupcakeScreen.Flavor.name)
        },
        modifier = Modifier
            .fillMaxSize()
            .padding(dimensionResource(R.dimen.padding_medium))
    )
}
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        options = DataSource.flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) },
        modifier = Modifier.fillMaxHeight()
    )
}
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState,
        onCancelButtonClicked = {
            cancelOrderAndNavigateToStart(viewModel, navController)
        },
        onSendButtonClicked = { subject: String, summary: String ->

        },
        modifier = Modifier.fillMaxHeight()
   )
}
  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 d'accueil. 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 ShareSheet (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. Pour un texte simple, vous pouvez utiliser "text/plain", bien que d'autres types, tels que "image/*" ou "video/*", soient 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.
import android.content.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.
import android.content.Intent

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.

a32e016a6ccbf427.png

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 ne répond 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 composable CupcakeAppBar doit connaître l'écran actuel pour que le titre soit correctement mis à jour.

  1. Dans l'énumération CupcakeScreen de CupcakeScreen.kt, ajoutez un paramètre de type Int nommé title à l'aide de l'annotation @StringRes.
import androidx.annotation.StringRes

enum class CupcakeScreen(@StringRes val title: Int) {
    Start,
    Flavor,
    Pickup,
    Summary
}
  1. Ajoutez une valeur de ressource pour chaque cas d'énumération, correspondant au texte du titre pour chaque écran. Utilisez app_name pour l'écran Start, choose_flavor pour l'écran Flavor, choose_pickup_date pour l'écran Pickup et order_summary pour l'écran Summary.
enum class CupcakeScreen(@StringRes val title: Int) {
    Start(title = R.string.app_name),
    Flavor(title = R.string.choose_flavor),
    Pickup(title = R.string.choose_pickup_date),
    Summary(title = R.string.order_summary)
}
  1. Ajoutez un paramètre nommé currentScreen de type CupcakeScreen au composable CupcakeAppBar.
fun CupcakeAppBar(
    currentScreen: CupcakeScreen,
    canNavigateBack: Boolean,
    navigateUp: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Dans CupcakeAppBar, remplacez le nom de l'application codé en dur par le titre de l'écran actuel en transmettant currentScreen.title à l'appel de stringResource() pour le paramètre de titre de TopAppBar.
TopAppBar(
    title = { Text(stringResource(currentScreen.title)) },
    modifier = modifier,
    navigationIcon = {
        if (canNavigateBack) {
            IconButton(onClick = navigateUp) {
                Icon(
                    imageVector = Icons.Filled.ArrowBack,
                    contentDescription = stringResource(R.string.back_button)
                )
            }
        }
    }
)

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 currentBackStackEntryAsState() de navController à l'aide du délégué by.
import androidx.navigation.compose.currentBackStackEntryAsState

@Composable
fun CupcakeApp(
    viewModel: OrderViewModel = viewModel(),
    navController: NavHostController = rememberNavController()
){

    val backStackEntry by navController.currentBackStackEntryAsState()

    ...
}
  1. Convertissez le titre de l'écran actuel en valeur CupcakeScreen. Sous la variable backStackEntry, créez une variable à l'aide de val nommée currentScreen égale au résultat de l'appel de la fonction de classe valueOf() de CupcakeScreen et transmettez l'itinéraire de la destination de backStackEntry. Utilisez l'opérateur Elvis pour fournir une valeur par défaut de CupcakeScreen.Start.name.
val currentScreen = CupcakeScreen.valueOf(
    backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
  1. Transmettez la valeur de la variable currentScreen au paramètre portant le même nom que le composable CupcakeAppBar.
CupcakeAppBar(
    currentScreen = currentScreen,
    canNavigateBack = false,
    navigateUp = {}
)

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.

3fd023516061f522.gif

8. Télécharger le code de solution

Pour télécharger le code de cet atelier de programmation terminé, utilisez les commandes Git suivantes :

$ 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 à un écran à 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