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
etColumn
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.
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.
- Dans
CupcakeScreen.kt
, au-dessus du composableCupcakeAppBar
, ajoutez une classe d'énumération nomméeCupcakeScreen
.
enum class CupcakeScreen() {
}
- Ajoutez quatre cas à la classe d'énumération :
Start
,Flavor
,Pickup
etSummary
.
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.
Il existe deux paramètres importants.
navController
: instance de la classeNavHostController
. Vous pouvez utiliser cet objet pour naviguer entre les écrans, par exemple en appelant la méthodenavigate()
pour accéder à une autre destination. Vous pouvez obtenirNavHostController
en appelantrememberNavController()
à 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 leNavHost
. Dans le cas de l'application Cupcake, il doit s'agir de la routeStart
.
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()
.
- Ouvrez
CupcakeScreen.kt
. - Dans
Scaffold
, sous la variableuiState
, ajoutez un composableNavHost
.
import androidx.navigation.compose.NavHost
Scaffold(
...
) { innerPadding ->
val uiState by viewModel.uiState.collectAsState()
NavHost()
}
- Transmettez la variable
navController
pour le paramètrenavController
etCupcakeScreen.Start.name
pour le paramètrestartDestination
. 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.
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érationCupcakeScreen
.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.
- Appelez la fonction
composable()
en transmettantCupcakeScreen.Start.name
pour laroute
.
import androidx.navigation.compose.composable
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
}
}
- Dans le lambda de fin, appelez le composable
StartOrderScreen
, en transmettantquantityOptions
pour la propriétéquantityOptions
. Pour la transmission demodifier
dansModifier.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))
)
}
}
- Sous le premier appel vers
composable()
, appelez à nouveaucomposable()
en transmettantCupcakeScreen.Flavor.name
pourroute
.
composable(route = CupcakeScreen.Flavor.name) {
}
- Dans le lambda de fin, obtenez une référence à
LocalContext.current
et stockez-la dans une variable nomméecontext
.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
}
- Appelez le composable
SelectOptionScreen
.
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
)
}
- 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ètresubtotal
.
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price
)
}
- 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 appelantcontext.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) }
)
}
- Pour le paramètre
onSelectionChanged
, transmettez une expression lambda qui appellesetFlavor()
sur le modèle de vue, en transmettantit
(l'argument transmis àonSelectionChanged()
). Pour le paramètremodifier
, transmettezModifier.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
.
- Appelez à nouveau la fonction
composable()
, en transmettantCupcakeScreen.Pickup.name
pour le paramètreroute
.
composable(route = CupcakeScreen.Pickup.name) {
}
- Dans le lambda de fin, appelez le composable
SelectOptionScreen
et transmettezuiState.price
pour lesubtotal
, comme précédemment. TransmettezuiState.pickupOptions
pour le paramètreoptions
et une expression lambda qui appellesetDate()
sur leviewModel
pour le paramètreonSelectionChanged
. Pour le paramètremodifier
, transmettez-le dansModifier.fillMaxHeight().
.
SelectOptionScreen(
subtotal = uiState.price,
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
- Appelez une nouvelle fois
composable()
, en transmettantCupcakeScreen.Summary.name
pourroute
.
composable(route = CupcakeScreen.Summary.name) {
}
- Dans le lambda de fin, appelez le composable
OrderSummaryScreen()
, en transmettant la variableuiState
pour le paramètreorderUiState
. Pour le paramètremodifier
, transmettez-le dansModifier.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.
- Ouvrez
StartOrderScreen.kt
. - 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
){
...
}
- Maintenant que le composable
StartOrderScreen
attend une valeur pouronNextButtonClicked
, recherchezStartOrderPreview
et transmettez un corps de lambda vide au paramètreonNextButtonClicked
.
@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.
- Modifiez le type du paramètre
onNextButtonClicked
pour qu'il accepte un paramètreInt
.
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
.
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()
.
- Recherchez l'expression lambda vide pour le paramètre
onClick
duSelectQuantityButton
.
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = {}
)
}
- Dans l'expression lambda, appelez
onNextButtonClicked
en transmettantitem.second
, soit le nombre de cupcakes.
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = { onNextButtonClicked(item.second) }
)
}
Ajouter des gestionnaires de bouton à SelectOptionScreen
- Sous le paramètre
onSelectionChanged
du composableSelectOptionScreen
deSelectOptionScreen.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
)
- 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
)
- Transmettez
onCancelButtonClicked
pour le paramètreonClick
du bouton "Cancel" (Annuler).
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
- Transmettez
onNextButtonClicked
pour le paramètreonClick
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.
- Dans le composable
OrderSummaryScreen
deSummaryScreen.kt
, ajoutez un paramètre nomméonCancelButtonClicked
, de type() -> Unit
.
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
modifier: Modifier = Modifier
){
...
}
- Ajoutez un autre paramètre de type
(String, String) -> Unit
et nommez-leonSendButtonClicked
.
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
onSendButtonClicked: (String, String) -> Unit,
modifier: Modifier = Modifier
){
...
}
- Le composable
OrderSummaryScreen
attend désormais des valeurs pouronSendButtonClicked
etonCancelButtonClicked
. RecherchezOrderSummaryPreview
, transmettez un corps de lambda vide avec deux paramètresString
àonSendButtonClicked
et un corps lambda vide aux paramètresonCancelButtonClicked
.
@Preview
@Composable
fun OrderSummaryPreview() {
CupcakeTheme {
OrderSummaryScreen(
orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
onSendButtonClicked = { subject: String, summary: String -> },
onCancelButtonClicked = {},
modifier = Modifier.fillMaxHeight()
)
}
}
- Transmettez
onSendButtonClicked
pour le paramètreonClick
du bouton Send (Envoyer). TransmetteznewOrder
etorderSummary
, les deux variables définies précédemment dansOrderSummaryScreen
. 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))
}
- Transmettez
onCancelButtonClicked
pour le paramètreonClick
du bouton Cancel (Annuler).
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
Accéder à une autre route
Pour accéder à une autre route, appelez simplement la méthode navigate()
sur votre instance de NavHostController
.
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
.
- Dans
CupcakeScreen.kt
, recherchez l'appel verscomposable()
pour l'écran de démarrage. Pour le paramètreonNextButtonClicked
, 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.
- Appelez
setQuantity
surviewModel
, en transmettantit
.
onNextButtonClicked = {
viewModel.setQuantity(it)
}
- Appelez
navigate()
surnavController
, en transmettantCupcakeScreen.Flavor.name
pour laroute
.
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
}
- Pour le paramètre
onNextButtonClicked
sur l'écran de choix du parfum, il suffit de transmettre un lambda qui appellenavigate()
, en transmettantCupcakeScreen.Pickup.name
pourroute
.
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()
)
}
- 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()
)
- Pour le paramètre
onNextButtonClicked
sur l'écran de sélection de la date de retrait, transmettez un lambda qui appellenavigate()
, en transmettantCupcakeScreen.Summary.name
pourroute
.
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()
)
}
- 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()
)
- Pour
OrderSummaryScreen
, transmettez des lambdas vides pouronCancelButtonClicked
etonSendButtonClicked
. Ajoutez des paramètres pour lesubject
et lesummary
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 () 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()
.
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).
- Après la fonction
CupcakeApp()
, définissez une fonction privée appeléecancelOrderAndNavigateToStart()
.
private fun cancelOrderAndNavigateToStart() {
}
- Ajoutez deux paramètres :
viewModel
de typeOrderViewModel
etnavController
de typeNavHostController
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
}
- Dans le corps de la fonction, appelez
resetOrder()
surviewModel
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
}
- Appelez
popBackStack()
surnavController
, en transmettantCupcakeScreen.Start.name
pourroute
etfalse
pourinclusive
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
- Dans le composable
CupcakeApp()
, transmettezcancelOrderAndNavigateToStart
pour les paramètresonCancelButtonClicked
des deux composablesSelectOptionScreen
et du composableOrderSummaryScreen
.
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()
)
}
- 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 :
- Créez un objet d'intent et spécifiez l'intent. Par exemple,
ACTION_SEND
. - 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. - 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
etEXTRA_TEXT
. - 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 :
- Dans CupcakeScreen.kt, sous le composable
CupcakeApp
, créez une fonction privée nomméeshareOrder()
.
private fun shareOrder()
- Ajoutez un paramètre nommé
context
, de typeContext
.
import android.content.Context
private fun shareOrder(context: Context) {
}
- Ajoutez deux paramètres
String
:subject
etsummary
. Ces chaînes seront affichées sur la feuille d'action de partage.
private fun shareOrder(context: Context, subject: String, summary: String) {
}
- Dans le corps de la fonction, créez un intent nommé
intent
et transmettezIntent.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.
- Appelez
apply()
sur le nouvel intent et transmettez une expression lambda.
val intent = Intent(Intent.ACTION_SEND).apply {
}
- 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"
}
- Appelez
putExtra()
, en transmettant l'objet pourEXTRA_SUBJECT
.
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
}
- Appelez
putExtra()
, en transmettant le récapitulatif pourEXTRA_TEXT
.
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, summary)
}
- Appelez la méthode de contexte
startActivity()
.
context.startActivity(
)
- Dans le lambda transmis à
startActivity()
, créez une activité à partir de l'intent en appelant la méthode de classecreateChooser()
. Transmettez l'intent pour le premier argument et la ressource de chaînenew_cupcake_order
.
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
- Dans le composable
CupcakeApp
, lors de l'appel verscomposable()
pourCucpakeScreen.Summary.name
, obtenez une référence à l'objet de contexte afin de pouvoir le transmettre à la fonctionshareOrder()
.
composable(route = CupcakeScreen.Summary.name) {
val context = LocalContext.current
...
}
- Dans le corps du lambda de
onSendButtonClicked()
, appelezshareOrder()
en transmettant les argumentscontext
,subject
etsummary
.
onSendButtonClicked = { subject: String, summary: String ->
shareOrder(context, subject = subject, summary = summary)
}
- 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.
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.
- Dans l'énumération
CupcakeScreen
de CupcakeScreen.kt, ajoutez un paramètre de typeInt
nommétitle
à l'aide de l'annotation@StringRes
.
import androidx.annotation.StringRes
enum class CupcakeScreen(@StringRes val title: Int) {
Start,
Flavor,
Pickup,
Summary
}
- Ajoutez une valeur de ressource pour chaque cas d'énumération, correspondant au texte du titre pour chaque écran. Utilisez
app_name
pour l'écranStart
,choose_flavor
pour l'écranFlavor
,choose_pickup_date
pour l'écranPickup
etorder_summary
pour l'écranSummary
.
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)
}
- Ajoutez un paramètre nommé
currentScreen
de typeCupcakeScreen
au composableCupcakeAppBar
.
fun CupcakeAppBar(
currentScreen: CupcakeScreen,
canNavigateBack: Boolean,
navigateUp: () -> Unit = {},
modifier: Modifier = Modifier
)
- Dans
CupcakeAppBar
, remplacez le nom de l'application codé en dur par le titre de l'écran actuel en transmettantcurrentScreen.title
à l'appel destringResource()
pour le paramètre de titre deTopAppBar
.
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".
- Dans le composable
CupcakeApp
, sous la variablenavController
, créez une variable nomméebackStackEntry
et appelez la méthodecurrentBackStackEntryAsState()
denavController
à 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()
...
}
- Convertissez le titre de l'écran actuel en valeur
CupcakeScreen
. Sous la variablebackStackEntry
, créez une variable à l'aide deval
nomméecurrentScreen
égale au résultat de l'appel de la fonction de classevalueOf()
deCupcakeScreen
et transmettez l'itinéraire de la destination debackStackEntry
. Utilisez l'opérateur Elvis pour fournir une valeur par défaut deCupcakeScreen.Start.name
.
val currentScreen = CupcakeScreen.valueOf(
backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
- Transmettez la valeur de la variable
currentScreen
au paramètre portant le même nom que le composableCupcakeAppBar
.
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é.
- Pour le paramètre
canNavigateBack
, transmettez une expression booléenne qui vérifie si la propriétépreviousBackStackEntry
denavController
n'est pas égale à la valeur "null".
canNavigateBack = navController.previousBackStackEntry != null,
- Pour revenir à l'écran précédent, appelez la méthode
navigateUp()
denavController
.
navigateUp = { navController.navigateUp() }
- 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.
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.