ViewModel partagé entre plusieurs fragments

1. Avant de commencer

Vous avez appris à utiliser les activités, les fragments, les intents, la liaison de données, les composants de navigation et les principes de base des composants d'architecture. Dans cet atelier de programmation, vous allez combiner tous ces apprentissages et travailler sur un exemple avancé : une application de commande de cupcakes.

Vous allez apprendre à utiliser un ViewModel partagé pour partager des données entre les fragments d'une même activité, et de nouveaux concepts comme les transformations LiveData.

Conditions préalables

  • Vous maîtrisez les mises en page Android au format XML.
  • Vous maîtrisez les principes de base du composant Navigation dans Jetpack.
  • Vous êtes capable de créer un graphique de navigation avec des destinations de fragment dans une application.
  • Vous avez déjà utilisé des fragments dans une activité.
  • Vous savez créer un ViewModel pour stocker les données de l'application.
  • Vous savez utiliser la liaison de données avec LiveData pour synchroniser l'interface utilisateur avec les données de l'application dans le ViewModel.

Points abordés

  • Mettre en œuvre les pratiques recommandées pour l'architecture des applications dans un cas d'utilisation plus avancé.
  • Utiliser une propriété ViewModel partagée entre les fragments d'une activité.
  • Appliquer une transformation LiveData.

Objectifs de l'atelier

  • L'application Cupcake, qui affiche un flux de commande de gâteaux et permet à l'utilisateur de choisir le goût, la quantité et la date de retrait de la commande.

Ce dont vous avez besoin

  • Un ordinateur sur lequel est installé Android Studio
  • Code de démarrage de l'application Cupcake.

2. Présentation de l'application de démarrage

Présentation de l'application Cupcake

L'application cupcake vous montre comment concevoir et implémenter une application de commande en ligne. À la fin de ce parcours, vous obtiendrez une application Cupcake composée des écrans suivants. L'utilisateur peut choisir la quantité, le goût et d'autres options lors de la commande.

732881cfc463695d.png

Télécharger le code de démarrage pour cet atelier de programmation

Cet atelier de programmation fournit un code de démarrage que vous pouvez étendre avec les fonctionnalités qui y sont enseignées. Le code de démarrage contient du code que vous avez déjà vu dans les ateliers de programmation précédents.

Si vous téléchargez le code de démarrage depuis GitHub, le nom du dossier du projet est android-basics-kotlin-cupcake-app-starter. Sélectionnez ce dossier lorsque vous ouvrirez le projet dans Android Studio.

Pour obtenir le code de cet atelier de programmation et l'ouvrir dans Android Studio, procédez comme suit :

Obtenir le code

  1. Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
  2. Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.

5b0a76c50478a73f.png

  1. Dans la boîte de dialogue, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
  2. Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
  3. Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.

Ouvrir le projet dans Android Studio

  1. Lancez Android Studio.
  2. Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).

36cc44fcf0f89a1d.png

Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).

21f3eec988dcfbe9.png

  1. Dans la boîte de dialogue Import Project (Importer un projet), accédez à l'emplacement du dossier du projet décompressé. Il se trouve probablement dans le dossier Téléchargements.
  2. Double-cliquez sur le dossier de ce projet.
  3. Attendez qu'Android Studio ouvre le projet.
  4. Cliquez sur le bouton Run (Exécuter) 11c34fc5e516fb1c.png pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
  5. Parcourez les fichiers du projet dans la fenêtre de l'outil Project (Projet) pour voir comment l'application est configurée.

Explication du code de démarrage

  1. Ouvrez le projet dans Android Studio. Le nom du dossier du projet est android-basics-kotlin-cupcake-app-starter. Ensuite, exécutez l'application.
  2. Parcourez les fichiers pour comprendre le code de démarrage. Pour les fichiers de mise en page, vous pouvez utiliser l'option Split (Diviser) disponible dans le coin supérieur droit pour afficher un aperçu de la mise en page et du fichier XML en même temps.
  3. En compilant et en exécutant l'application, vous remarquerez qu'elle est incomplète. Les boutons ne font rien de spécial (sauf afficher un message Toast) et vous ne pouvez pas naviguer vers les autres fragments.

Voici une présentation des fichiers importants du projet.

MainActivity :

Le code MainActivity est semblable au code généré par défaut, qui définit la vue du contenu de l'activité sur activity_main.xml. Ce code utilise un constructeur AppCompatActivity(@LayoutRes int contentLayoutId) paramétré qui accepte une mise en page qui sera gonflée dans le cadre de super.onCreate(savedInstanceState).

Le code dans la classe MainActivity

class MainActivity : AppCompatActivity(R.layout.activity_main)

est identique au code suivant qui utilise le constructeur AppCompatActivity par défaut :

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
   }
}

Mises en page (dossier res/layout) :

Le dossier de ressources layout contient des fichiers de mise en page d'activité et de fragment. Il s'agit de fichiers de mise en page simples, et le format XML a déjà été utilisé lors des précédents ateliers de programmation.

  • fragment_start.xml est le premier écran qui s'affiche dans l'application. Il contient une image représentant un cupcake et trois boutons permettant de choisir le nombre de cupcakes à commander : un, six et douze cupcakes.
  • fragment_flavor.xml affiche une liste de types de cupcakes sous la forme de cases d'option, et un bouton Next (Suivant).
  • fragment_pickup.xml permet de sélectionner le jour du retrait de la commande, le bouton Suivant d'accéder à l'écran récapitulatif.
  • fragment_summary.xml affiche un récapitulatif des détails de la commande, y compris la quantité, le type de produit et un bouton permettant d'envoyer la commande vers une autre application.

Classes de fragments :

  • StartFragment.kt est le premier écran qui s'affiche dans l'application. Cette classe contient le composant View Binding et un gestionnaire de clics pour les trois boutons.
  • Les classes FlavorFragment.kt, PickupFragment.kt et SummaryFragment.kt contiennent du code récurrent et un gestionnaire de clics pour les boutons Next (Suivant) ou Send Order to Another App (Envoyer la commande vers une autre application). Ces deux éléments affichent un message toast.

Ressources (dossier res) :

  • Le dossier drawable contient l'élément cupcake pour le premier écran, ainsi que les fichiers de l'icône de lanceur.
  • navigation/nav_graph.xml contient quatre destinations de fragment (startFragment, flavorFragment, pickupFragment et summaryFragment), sans actions, que vous définirez ultérieurement dans l'atelier.
  • Le dossier values contient les couleurs, les dimensions, les chaînes, les styles et les thèmes utilisés pour personnaliser le thème de l'application. Ces types de ressources devraient vous être familiers suite aux précédents ateliers.

3. Compléter le graphique de navigation

Dans cette tâche, vous allez connecter les écrans de l'application Cupcake et terminer l'implémentation de la navigation dans l'application.

Vous souvenez-vous de ce dont nous avons besoin pour utiliser le composant Navigation ? Suivez ce guide pour obtenir un rappel sur différents points pour votre projet et votre application, de façon à :

  • inclure la bibliothèque de navigation Jetpack ;
  • ajouter un NavHost à l'activité ;
  • créer un graphique de navigation ;
  • ajouter des destinations de fragment au graphique de navigation.

Connecter des destinations dans le graphique de navigation

  1. Dans Android Studio, dans la fenêtre Project (Projet), accédez au fichier res > navigation > nav_graph.xml. Accédez à l'onglet Design (Conception) s'il n'est pas déjà sélectionné.

28c2c94eb97e2f0.png

  1. L'éditeur de navigation s'ouvre : il affiche le graphique de navigation dans votre application, et les quatre fragments qui existent déjà dans l'application.

fdce89b318218ea6.png

  1. Connectez les destinations de fragment dans le graphique de navigation. Créez une action de startFragment à flavorFragment, une connexion de flavorFragment à pickupFragment, ainsi qu'une connexion de pickupFragment à summaryFragment. Suivez ces étapes si vous avez besoin d'instructions plus détaillées.
  2. Pointez sur startFragment jusqu'à ce que la bordure grise entoure le fragment et que le cercle gris apparaisse au centre du bord droit du fragment. Cliquez sur le cercle et faites-le glisser vers flavorFragment, puis relâchez le bouton de la souris.

d014c1b710c1088d.png

  1. La flèche entre les deux fragments indique une connexion réussie, ce qui signifie que vous pourrez naviguer du startFragment au flavorFragment. C'est ce qu'on appelle une action de navigation, une notion déjà abordée lors d'un précédent atelier.

65c7d993b98c9dea.png

  1. De la même façon, ajoutez les actions de navigation de flavorFragment à pickupFragment, et de pickupFragment à summaryFragment. Une fois ces actions de navigation créées, le graphique de navigation terminé devrait se présenter comme suit.

724eb8992a1a9381.png

  1. Les trois actions que vous avez créées doivent également apparaître dans l'arborescence des composants.

e4ee54469f5ff1a4.png

  1. Lorsque vous définissez un graphique de navigation, vous devez également spécifier la destination de départ. Comme vous pouvez le voir ici, une icône représentant une maison apparaît à côté de startFragment.

739d4ddac561c478.png

Cela indique que startFragment sera le premier fragment à être affiché dans NavHost. Conservez le comportement souhaité pour notre application. Pour référence ultérieure, vous pouvez toujours modifier la destination de départ en effectuant un clic droit sur un fragment, puis en sélectionnant l'option de menu Set as Start Destination (Définir comme destination de départ).

bf3cfa7841476892.png

Vous allez ensuite ajouter du code pour passer de startFragment à flavorFragment en appuyant sur les boutons du premier fragment, au lieu d'afficher un message Toast. Vous trouverez ci-dessous la référence à la mise en page de l'élément startFragment. Vous transmettrez la quantité de cupcakes à l'élément flavorFragment lors d'une prochaine tâche.

867d8e4c72078f76.png

  1. Dans la fenêtre Projet, ouvrez le fichier Kotlin app > java > com.example.cupcake > StartFragment.
  2. Dans la méthode onViewCreated(), notez que les écouteurs de clics sont définis sur les trois boutons. Lorsque vous appuyez sur chaque bouton, la méthode orderCupcake() est appelée avec le nombre de cupcakes (1, 6 ou 12 cupcakes), qui est son paramètre.

Code de référence :

orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
  1. Dans la méthode orderCupcake(), remplacez le code qui affiche le message de toast par le code permettant d'accéder au fragment "flavor". Obtenez le NavController utilisant la méthode findNavController() et appelez navigate() en lui transmettant l'ID de l'action,R.id.action_startFragment_to_flavorFragment. Assurez-vous que cet ID d'action correspond à l'action déclarée dans votre nav_graph.xml..

Remplacer

fun orderCupcake(quantity: Int) {
    Toast.makeText(activity, "Ordered $quantity cupcake(s)", Toast.LENGTH_SHORT).show()
}

avec

fun orderCupcake(quantity: Int) {
   findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Ajoutez le fichier import d'importation androidx.navigation.fragment.findNavController ou faites votre choix parmi les options fournies par Android Studio.

2a087f53a77765a6.png

Ajouter la navigation aux fragments "flavor" et "pickup"

Comme pour la tâche précédente, vous allez ajouter la navigation aux autres fragments : "flavor" et "pickup".

3b351067bf4926b7.png

  1. Ouvrez app > java > com.example.cupcake > FlavorFragment.kt. La méthode appelée dans l'écouteur de clics du bouton Suivant est goToNextScreen().
  2. Dans FlavorFragment.kt, dans la méthode goToNextScreen(), remplacez le code affichant le toast pour naviguer vers l'élément pickupFragment. Utilisez l'ID d'action R.id.action_flavorFragment_to_pickupFragment et assurez-vous qu'il correspond à l'action déclarée dans votre fichier nav_graph.xml..
fun goToNextScreen() {
    findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}

Pensez à importer l'élément import androidx.navigation.fragment.findNavController.

  1. De même, dans PickupFragment.kt, dans la méthode goToNextScreen(), remplacez le code existant pour accéder à l'élément summaryFragment.
fun goToNextScreen() {
    findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}

Importez androidx.navigation.fragment.findNavController.

  1. Exécutez l'application. Vérifiez que les boutons fonctionnent bien pour naviguer d'un écran à l'autre. Les informations affichées sur chaque fragment peuvent être incomplètes, mais ne vous inquiétez pas, les bonnes données y seront insérées lors des prochaines étapes.

96b33bf7a5bd8050.png

Modifier le titre dans la barre d'application

Lorsque vous naviguez dans l'application, son titre s'affiche dans la barre d'application. Il s'affiche toujours comme suit : Cupcake.

Il serait judicieux de fournir un titre plus pertinent pour la fonctionnalité du fragment en cours d'utilisation.

Dans la barre d'application (également appelée barre d'action), modifiez le titre de chaque fragment à l'aide du NavController et affichez le bouton Haut (←).

b7657cdc50cfeab0.png

  1. Dans MainActivity.kt, remplacez la méthode onCreate() pour configurer le contrôleur de navigation. Obtenez une instance de NavController à partir de NavHostFragment.
  2. Appelez setupActionBarWithNavController(navController) en transmettant NavController dans l'instance. Cette option permet d'afficher un titre dans la barre d'application en fonction du libellé de la destination, et d'afficher le bouton Haut lorsque vous ne vous trouvez pas sur une destination de niveau supérieur.
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }
}
  1. Ajoutez les importations nécessaires lorsque Android Studio vous le demande.
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
  1. Définissez les titres de la barre d'application pour chaque fragment. Ouvrez navigation/nav_graph.xml et passez à l'onglet Code.
  2. Dans nav_graph.xml, modifiez l'attribut android:label pour chaque destination de fragment. Utilisez les ressources de chaîne suivantes, qui ont déjà été déclarées dans l'application de démarrage.

Pour l'élément startFragment, utilisez @string/app_name avec la valeur Cupcake.

Pour l'élément flavorFragment, utilisez @string/choose_flavor avec la valeur Choose Flavor.

Pour l'élément pickupFragment, utilisez @string/choose_pickup_date avec la valeur Choose Pickup Date.

Pour le fragment order_summary, utilisez @string/order_summary avec la valeur Order Summary.

<navigation ...>
    <fragment
        android:id="@+id/startFragment"
        ...
        android:label="@string/app_name" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/flavorFragment"
        ...
        android:label="@string/choose_flavor" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/pickupFragment"
        ...
        android:label="@string/choose_pickup_date" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/summaryFragment"
        ...
        android:label="@string/order_summary" ... />
</navigation>
  1. Exécutez l'application. Notez que le titre dans la barre d'application change lorsque vous naviguez d'une destination de fragment à l'autre. Notez également que le bouton Haut (flèche ←) s'affiche désormais dans la barre d'application. Si vous appuyez dessus, il ne se passe rien. Vous implémenterez le comportement du bouton Haut dans le prochain atelier de programmation.

89e0ea37d4146271.png

4. Créer un ViewModel partagé

Passons à la saisie des données correctes dans chacun des fragments. Vous allez utiliser un ViewModel partagé pour enregistrer les données de l'application dans un seul ViewModel. Plusieurs fragments de l'application accéderont au ViewModel partagé en fonction du champ d'application de leur activité.

Dans la plupart des applications de production, il est courant de partager des données entre les fragments. Par exemple, dans la version finale de l'application Cupcake (pour cet atelier, voir les captures d'écran ci-dessous), l'utilisateur sélectionne la quantité de cupcakes dans le premier écran. Dans le second, le prix est calculé et affiché en fonction de la quantité saisie. De même, d'autres données de l'application, telles que la saveur et la date de retrait, sont également utilisées dans l'écran récapitulatif.

3b6a68cab0b9ee2.png

En examinant les caractéristiques de l'application, il peut sembler utile de stocker ces informations de commande dans un seul ViewModel, qui peut être partagé entre les différents fragments de cette activité. N'oubliez pas que ViewModel fait partie des composants d'architecture Android. Les données de l'application enregistrées dans ViewModel sont conservées lors des modifications de configuration. Pour ajouter un ViewModel à votre application, vous devez créer une classe basée sur la classe ViewModel.

Créer un OrderViewModel

Dans cette tâche, vous allez créer un ViewModel partagé pour l'application Cupcake, appelé OrderViewModel. Vous allez également ajouter les données de l'application en tant que propriétés dans ViewModel, ainsi que des méthodes pour mettre à jour et modifier les données. Voici les propriétés de la classe :

  • Quantité de la commande (Integer)
  • Saveur du cupcake (String)
  • Date de retrait (String)
  • Prix (Double)

Suivre les bonnes pratiques de ViewModel

Dans un ViewModel, il est recommandé de ne pas exposer les données du ViewModel en tant que variables public. Sinon, les données des applications peuvent être modifiées de manière inattendue par les classes externes et créer des cas particuliers que votre application ne s'attendait pas à gérer. Définissez plutôt ces propriétés modifiables private, implémentez une propriété de sauvegarde et exposez une version immuable public de chaque propriété, si nécessaire. La convention consiste à ajouter un trait de soulignement (_) au nom des propriétés modifiables private.

Voici les méthodes permettant de mettre à jour les propriétés ci-dessus, selon le choix de l'utilisateur :

  • setQuantity(numberCupcakes: Int)
  • setFlavor(desiredFlavor: String)
  • setDate(pickupDate: String)

Vous n'avez pas besoin d'une méthode "setter" pour le prix, car vous le calculerez dans la OrderViewModel à l'aide d'autres propriétés. Les étapes ci-dessous vous expliquent comment implémenter le ViewModel partagé.

Dans votre projet, vous allez créer un package appelé model et ajouter la classe OrderViewModel. Le code du ViewModel sera ainsi séparé du reste du code de l'interface utilisateur (fragments et activités). Il est recommandé de diviser le code en packages selon la fonctionnalité.

  1. Dans la fenêtre Projet d'Android Studio, effectuez un clic droit sur com.example.cupcake > Nouveau > Package.
  2. Une boîte de dialogue Nouveau package s'ouvre. Attribuez le nom com.example.cupcake.model au package.

d958ee5f3d2aef5a.png

  1. Créez la classe Kotlin OrderViewModel sous le package model. Dans la fenêtre Projet, effectuez un clic droit sur le package model, puis sélectionnez Nouveau > Fichier/Classe Kotlin. Dans la boîte de dialogue qui s'ouvre, nommez le fichier OrderViewModel.

fc68c1d3861f1cca.png

  1. Dans OrderViewModel.kt, modifiez la signature de la classe pour qu'elle se base sur le ViewModel.
import androidx.lifecycle.ViewModel

class OrderViewModel : ViewModel() {

}
  1. Dans la classe OrderViewModel, ajoutez les propriétés décrites ci-dessus en tant que propriété private val.
  2. Définissez les types de propriétés sur LiveData et ajoutez des champs de stockages aux propriétés, afin qu'elles puissent être observables et que l'UI puisse être mise à jour lorsque les données sources du ViewModel sont modifiées.
private val _quantity = MutableLiveData<Int>(0)
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>("")
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>("")
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>(0.0)
val price: LiveData<Double> = _price

Vous devez importer les classes suivantes :

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
  1. Dans la classe OrderViewModel, ajoutez les méthodes décrites ci-dessus. Dans les méthodes, attribuez l'argument transmis aux propriétés modifiables.
  2. Étant donné que ces méthodes Setter doivent être appelées depuis l'extérieur du ViewModel, conservez-les en tant que méthodes public. Cela signifie qu'aucun private ni autre modificateur de visibilité n'est requis avant le mot clé fun. En Kotlin, le modificateur de visibilité par défaut est public.
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
}

fun setFlavor(desiredFlavor: String) {
    _flavor.value = desiredFlavor
}

fun setDate(pickupDate: String) {
    _date.value = pickupDate
}
  1. Créez et exécutez votre application pour vous assurer qu'elle ne présente aucune erreur de compilation. L'interface ne devrait pas encore être modifiée.

Bien joué ! Vous êtes maintenant prêt à créer votre ViewModel. Vous ajouterez progressivement des éléments à cette classe au fil du développement des fonctionnalités de votre application, et lorsque vous constaterez le besoin de recourir à d'autres propriétés et méthodes dans votre classe.

Si le nom des classes, des propriétés ou des méthodes s'affiche en gris dans Android Studio, c'est normal. Cela signifie que la classe, les propriétés ou les méthodes ne sont pas utilisées pour le moment. Pour le moment seulement ! C'est la prochaine étape.

5. Utiliser ViewModel pour mettre à jour l'UI

Dans cette tâche, vous allez utiliser le ViewModel partagé que vous avez créé pour mettre à jour l'interface utilisateur de l'application. La principale différence dans l'implémentation d'un ViewModel partagé réside dans la façon dont nous y accédons depuis les contrôleurs d'UI. Vous allez utiliser l'instance d'activité à la place de l'instance du fragment. Nous vous expliquerons comment procéder dans les sections suivantes.

Le ViewModel peut donc être partagé entre plusieurs fragments. Chaque fragment peut accéder au ViewModel pour vérifier certains détails de la commande ou mettre à jour certaines données du ViewModel.

Mettre à jour StartFragment pour utiliser le ViewModel

Pour utiliser le ViewModel partagé dans StartFragment, initialisez OrderViewModel à l'aide de activityViewModels() au lieu de la classe déléguée viewModels().

  • viewModels() vous donne l'instance ViewModel limitée au fragment actuel. La procédure diffère selon les fragments.
  • activityViewModels() vous donne l'instance ViewModel limitée à l'activité actuelle. Par conséquent, l'instance reste la même sur plusieurs fragments de la même activité.

Utiliser la délégation de propriété Kotlin

En Kotlin, chaque propriété modifiable (var) dispose automatiquement de fonctions "getter" et "setter". Ces fonctions sont appelées lorsque vous attribuez une valeur à la propriété ou la lisez. (Pour une propriété en lecture seule (val), seule la fonction "getter" est générée par défaut. Cette fonction getter est appelée lorsque vous lisez la valeur d'une propriété en lecture seule.)

La délégation de propriété en Kotlin vous aide à transférer la responsabilité "getter-setter" à une autre classe.

Cette classe (appelée classe déléguée) fournit les fonctions getter et setter de la propriété et gère ses modifications.

Un délégué de propriété est défini à l'aide de la clause by et d'une instance de classe déléguée :

// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
  1. Dans la classe StartFragment, obtenez une référence au ViewModel partagé en tant que variable de classe. Utilisez le délégué de propriété Kotlin by activityViewModels() de la bibliothèque fragment-ktx.
private val sharedViewModel: OrderViewModel by activityViewModels()

Vous aurez peut-être besoin de ces nouvelles importations :

import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
  1. Répétez l'étape ci-dessus pour les classes FlavorFragment, PickupFragment et SummaryFragment. Vous utiliserez cette instance sharedViewModel dans les sections suivantes de l'atelier de programmation.
  2. Revenez à la classe StartFragment. Vous pouvez maintenant utiliser le ViewModel. Au début de la méthode orderCupcake(), appelez la méthode setQuantity() dans le ViewModel partagé pour mettre à jour la quantité, avant d'accéder au flavorFragment.
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Dans la classe OrderViewModel, ajoutez la méthode suivante pour vérifier si la saveur du produit commandé a été définie ou non. Vous utiliserez cette méthode dans la classe StartFragment lors des prochaines tâches.
fun hasNoFlavorSet(): Boolean {
    return _flavor.value.isNullOrEmpty()
}
  1. Dans la classe StartFragment, dans la méthode orderCupcake(), après avoir défini la quantité, définissez "Vanille" comme saveur par défaut si aucun parfum n'est défini, avant d'accéder au flavorFragment. Une fois la méthode terminée, elle doit se présenter comme suit :
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    if (sharedViewModel.hasNoFlavorSet()) {
        sharedViewModel.setFlavor(getString(R.string.vanilla))
    }
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Créez votre application pour vous assurer qu'elle ne présente aucune erreur de compilation. L'interface ne devrait pas être modifiée.

6. Utiliser ViewModel avec la liaison de données

Vous allez ensuite utiliser la liaison de données pour lier les données du ViewModel à l'interface utilisateur. Vous mettrez également à jour le ViewModel partagé en fonction des sélections effectuées par l'utilisateur dans l'interface utilisateur.

Rappel sur la liaison de données

Rappelez-vous que la Bibliothèque de liaison de données fait partie d'Android Jetpack. La liaison de données lie les composants d'interface utilisateur dans vos mises en page aux sources de données de votre application à l'aide d'un format déclaratif. Pour faire simple, la liaison de données consiste à lier les données (à partir du code) aux vues et à lier les affichages (liaison d'affichages au code). En configurant ces liaisons et en rendant les mises à jour automatiques, vous réduisez le risque d'erreurs si vous oubliez de mettre à jour manuellement l'UI de votre code.

Modifier le type de produit selon le choix de l'utilisateur

  1. Dans layout/fragment_flavor.xml, ajoutez une balise <data> dans la balise racine <layout>. Ajoutez une variable de mise en page nommée viewModel de type com.example.cupcake.model.OrderViewModel. Assurez-vous que le nom du package dans l'attribut de type correspond au nom du package de la classe du ViewModel partagé, OrderViewModel, dans votre application.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. De même, répétez l'étape ci-dessus pour fragment_pickup.xml et fragment_summary.xml pour ajouter la variable de mise en page viewModel. Vous utiliserez cette variable dans les sections suivantes. Vous n'avez pas besoin d'ajouter ce code dans fragment_start.xml, car cette mise en page n'utilise pas le ViewModel partagé.
  2. Dans la classe FlavorFragment, dans onViewCreated(), liez l'instance du ViewModel à l'instance du ViewModel partagé de la mise en page. Ajoutez le code suivant dans le bloc binding?.apply.
binding?.apply {
    viewModel = sharedViewModel
    ...
}

Appliquer la fonction Scope

C'est la première fois que vous rencontrez la fonction apply en langage Kotlin. apply est une fonction de champ d'application appartenant à la bibliothèque standard Kotlin. Elle exécute un bloc de code dans le contexte d'un objet. Il s'agit d'un champ d'application temporaire qui vous permet d'accéder à l'objet sans son nom. Le principal cas d'utilisation de apply est la configuration d'un objet. Ces appels peuvent être lus comme suit : "appliquer les attributions suivantes à l'objet".

Exemple :

clark.apply {
    firstName = "Clark"
    lastName = "James"
    age = 18
}

// The equivalent code without apply scope function would look like the following.

clark.firstName = "Clark"
clark.lastName = "James"
clark.age = 18
  1. Répétez la même étape pour la méthode onViewCreated() dans les classes PickupFragment et SummaryFragment.
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. Dans fragment_flavor.xml, utilisez la nouvelle variable de mise en page viewModel pour définir l'attribut checked des cases d'option en fonction de la valeur flavor du ViewModel. Si la saveur représentée par une case d'option est identique à celle enregistrée dans le ViewModel, affichez la case d'option sélectionnée (checked = true). L'expression de liaison de l'état coché du curseur RadioButton Vanille se présente comme suit :

@{viewModel.flavor.equals(@string/vanilla)}

Globalement, vous comparez la propriété viewModel.flavor à la ressource de chaîne correspondante à l'aide de la fonction equals pour déterminer si l'état coché doit être vrai ou faux.

<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:checked="@{viewModel.flavor.equals(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:checked="@{viewModel.flavor.equals(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:checked="@{viewModel.flavor.equals(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:checked="@{viewModel.flavor.equals(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:checked="@{viewModel.flavor.equals(@string/coffee)}"
       .../>
</RadioGroup>

Expressions "listener binding"

Les expressions "listener binding" sont des expressions lambda qui s'exécutent lorsqu'un événement se produit, comme lors d'un événement onClick. Elles sont semblables aux références de méthodes telles que textview.setOnClickListener(clickListener), mais elles vous permettent d'exécuter des expressions de liaison de données arbitraires.

  1. Dans fragment_flavor.xml, ajoutez des écouteurs d'événements aux cases d'option à l'aide des expressions "listener binding". Utilisez une expression lambda sans paramètre et appelez viewModel.setFlavor() en transmettant la ressource de chaîne de saveur correspondante.
<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/coffee)}"
       .../>
</RadioGroup>
  1. Exécutez l'application. Notez que l'option Vanilla est sélectionnée par défaut dans le fragment.

3095e824b4817b98.png

Parfait ! Vous pouvez maintenant passer aux fragments suivants.

7. Mettre à jour le fragment "pickup" et "summary" pour utiliser le ViewModel

Parcourez l'application et notez que dans le pickupFragment, les libellés des cases d'option sont vides. Dans cette tâche, vous allez calculer les quatre dates de retrait disponibles et les afficher dans le pickupFragment. Il existe plusieurs façons d'afficher une date mise en forme. Voici quelques utilitaires fournis par Android pour vous aider à effectuer cette opération.

Créer une liste d'options de retrait

Outil de mise en forme des dates

Le framework Android fournit une classe appelée SimpleDateFormat, qui permet de mettre en forme et d'analyser les dates en tenant compte des paramètres régionaux. Elle permet de mettre en forme (date → texte) et d'analyser les dates (texte → date).

Vous pouvez créer une instance de SimpleDateFormat en transmettant une chaîne de modèle et un paramètre régional :

SimpleDateFormat("E MMM d", Locale.getDefault())

Une chaîne de format comme "E MMM d" est une représentation des formats Date et Heure. Les lettres de 'A' à 'Z' et de 'a' à 'z' sont interprétées comme des lettres représentant des composants d'une chaîne de date ou d'heure. Par exemple, d représente le jour du mois, y l'année et M le mois. Si la date est le 4 janvier 2018, la chaîne de modèle "EEE, MMM d" analyse "Wed, Jul 4". Pour obtenir la liste complète des modèles, consultez la documentation.

Un objet Locale représente une région géographique, politique ou culturelle spécifique. Il représente une combinaison de langue, pays et variante. Les paramètres régionaux permettent de modifier la présentation des informations (comme les nombres ou les dates) pour suivre les différentes conventions régionales. La date et l'heure sont sensibles aux paramètres régionaux, car ils s'écrivent différemment selon les régions du monde. Vous allez utiliser la méthode Locale.getDefault() pour récupérer les informations sur les paramètres régionaux définis sur l'appareil de l'utilisateur et les transmettre au constructeur SimpleDateFormat.

Les paramètres régionaux dans Android sont composés de la langue et du code pays. Il s'agit de codes ISO à deux lettres en minuscules, comme "en" pour l'anglais. Les codes pays sont des codes pays ISO à deux lettres en majuscules, comme "US" pour les États-Unis.

Utilisez maintenant SimpleDateFormat et Locale pour déterminer les dates de retrait disponibles pour l'application Cupcake.

  1. Dans la classe OrderViewModel, ajoutez la fonction getPickupOptions() ci-dessous pour créer et renvoyer la liste des dates de retrait. Dans la méthode, créez une variable val appelée options et initialisez-la sur mutableListOf<String>().
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
}
  1. Créez une chaîne de mise en forme à l'aide de SimpleDateFormat en transmettant la chaîne de modèle "E MMM d" et les paramètres régionaux. Dans la chaîne du format, E correspond au jour de la semaine et analyse la requête mardi 10 décembre.
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())

Importez java.text.SimpleDateFormat et java.util.Locale lorsqu'Android Studio vous le demande.

  1. Obtenez une instance Calendar et affectez-la à une nouvelle variable. Définissez-la comme val. Cette variable contiendra la date et l'heure actuelles. Importez également java.util.Calendar.
val calendar = Calendar.getInstance()
  1. Créez une liste de dates en commençant par la date du jour et les trois dates suivantes. Comme vous aurez besoin de quatre options de date, répétez ce bloc de code quatre fois. Ce bloc repeat met en forme une date, l'ajoute à la liste des options de date, puis incrémente le calendrier d'un jour.
repeat(4) {
    options.add(formatter.format(calendar.time))
    calendar.add(Calendar.DATE, 1)
}
  1. Renvoyez la options mise à jour à la fin de la méthode. Voici votre méthode complète :
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
   val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
   val calendar = Calendar.getInstance()
   // Create a list of dates starting with the current date and the following 3 dates
   repeat(4) {
       options.add(formatter.format(calendar.time))
       calendar.add(Calendar.DATE, 1)
   }
   return options
}
  1. Dans la classe OrderViewModel, ajoutez une propriété de classe appelée dateOptions qui est une val. Initialisez-la à l'aide de la méthode getPickupOptions() que vous venez de créer.
val dateOptions = getPickupOptions()

Modifier la mise en page pour afficher les options de retrait

Maintenant que les quatre dates de retrait sont disponibles dans le ViewModel, mettez à jour la mise en page fragment_pickup.xml pour les afficher. Vous utiliserez également la liaison de données pour afficher l'état coché de chaque case d'option et pour mettre à jour la date dans le ViewModel lorsqu'une autre case d'option est sélectionnée. Cette implémentation est semblable à la liaison de données dans le fragment "flavor".

Dans fragment_pickup.xml :

Case d'option option0 représentant dateOptions[0] dans viewModel (aujourd'hui)

Case d'option option1 représentant dateOptions[1] dans viewModel (demain)

Case d'option option2 représentant dateOptions[2] dans viewModel (le surlendemain)

Case d'option option3 représentant dateOptions[3] en viewModel (le lendemain du surlendemain)

  1. Dans fragment_pickup.xml, pour la case d'option option0, utilisez la nouvelle variable de mise en page viewModel pour définir l'attribut checked en fonction de la valeur date dans le ViewModel. Comparez la propriété viewModel.date à la première chaîne de la liste dateOptions, qui correspond à la date du jour. Utilisez la fonction equals pour comparer. L'expression de liaison finale se présente comme suit :

@{viewModel.date.equals(viewModel.dateOptions[0])}

  1. Pour la même case d'option, ajoutez un écouteur d'événements à l'aide d'une expression "listener binding" sur l'attribut onClick. Lorsque vous cliquez sur cette option, appelez setDate() sur le viewModel, en transmettant dateOptions[0].
  2. Pour la même case d'option, définissez la valeur de l'attribut text sur la première chaîne de la liste dateOptions.
<RadioButton
   android:id="@+id/option0"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[0])}"
   android:text="@{viewModel.dateOptions[0]}"
   ...
   />
  1. Répétez les étapes ci-dessus pour les autres cases d'option, et modifiez l'index de dateOptions en conséquence.
<RadioButton
   android:id="@+id/option1"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[1])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[1])}"
   android:text="@{viewModel.dateOptions[1]}"
   ... />

<RadioButton
   android:id="@+id/option2"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[2])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[2])}"
   android:text="@{viewModel.dateOptions[2]}"
   ... />

<RadioButton
   android:id="@+id/option3"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[3])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[3])}"
   android:text="@{viewModel.dateOptions[3]}"
   ... />
  1. Exécutez l'application. Les options de retrait disponibles correspondent alors aux prochains jours. Votre capture d'écran varie en fonction de la date du jour. Notez qu'aucune option n'est sélectionnée par défaut. Vous l'implémenterez à l'étape suivante.

b55b3a36e2aa7be6.png

  1. Dans la classe OrderViewModel, créez une fonction appelée resetOrder() pour réinitialiser les propriétés MutableLiveData dans le ViewModel. Définissez la valeur de date actuelle de la liste dateOptions sur _date.value..
fun resetOrder() {
   _quantity.value = 0
   _flavor.value = ""
   _date.value = dateOptions[0]
   _price.value = 0.0
}
  1. Ajoutez un bloc init à la classe et appelez la nouvelle méthode resetOrder() à partir de celle-ci.
init {
   resetOrder()
}
  1. Supprimez les valeurs initiales de la déclaration des propriétés de la classe. Vous utilisez maintenant le bloc init pour initialiser les propriétés lorsqu'une instance de OrderViewModel est créée.
private val _quantity = MutableLiveData<Int>()
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>()
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>()
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>()
val price: LiveData<Double> = _price
  1. Exécutez à nouveau votre application. Notez que la date du jour est sélectionnée par défaut.

bfe4f1b82977b4bc.png

Mettre à jour le fragment Résumé pour utiliser le ViewModel

Passons maintenant au dernier fragment. Le fragment "order_summary" est destiné à afficher un récapitulatif des détails de la commande. Dans cette tâche, vous allez exploiter toutes les informations de commande du ViewModel partagé et mettre à jour les détails des commandes à l'écran à l'aide de la liaison de données.

78f510e10d848dd2.png

  1. Dans fragment_summary.xml, vérifiez que la variable de données du ViewModel, viewModel, a été déclarée.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. Dans SummaryFragment, dans onViewCreated(), assurez-vous que binding.viewModel est initialisé.
  2. Dans fragment_summary.xml, lisez le ViewModel pour mettre à jour l'écran avec le détail du récapitulatif de commande. Modifiez la quantité, le type de produit et la date TextViews en ajoutant les attributs textuels suivants. La quantité est du type Int. Vous devez donc la convertir en chaîne.
<TextView
   android:id="@+id/quantity"
   ...
   android:text="@{viewModel.quantity.toString()}"
   ... />
<TextView
   android:id="@+id/flavor"
   ...
   android:text="@{viewModel.flavor}"
   ... />
<TextView
   android:id="@+id/date"
   ...
   android:text="@{viewModel.date}"
   ... />
  1. Exécutez et testez l'application pour vérifier que les options de commande sélectionnées s'affichent dans le récapitulatif des commandes.

7091453fa817b55.png

8. Calculer le prix à partir des détails de la commande

En regardant les dernières captures d'écran de cet atelier de programmation, vous remarquerez que le prix est affiché sur chaque fragment (à l'exception de StartFragment) afin que l'utilisateur connaisse le prix lorsqu'il crée la commande.

3b6a68cab0b9ee2.png

Voici les règles de notre boutique de cupcakes pour calculer le prix.

  • Chaque cupcake coûte 2 $.
  • Le retrait le jour même ajoute 3 $ à la commande.

Par conséquent, pour une commande de six cupcakes, le prix est de six cupcakes x 2 $ chacun = 12 $. Si l'utilisateur souhaite retirer sa commande le jour même, le coût supplémentaire de 3 $ s'ajoute. Le montant total de la commande s'élève alors à 15 $.

Mettre à jour le prix dans le ViewModel

Pour que cette fonctionnalité soit disponible dans votre application, indiquez d'abord le prix par cupcake et ignorez le coût du retrait le jour même.

  1. Ouvrez OrderViewModel.kt et stockez le prix par cupcake dans une variable. Déclarez-la en tant que constante privée de premier niveau en haut du fichier, en dehors de la définition de classe (mais après les instructions d'importation). Utilisez le modificateur const, pour le transformer en modificateur en lecture seule, utilisez val.
package ...

import ...

private const val PRICE_PER_CUPCAKE = 2.00

class OrderViewModel : ViewModel() {
    ...

Rappelez-vous que les valeurs constantes (identifiées par le mot clé const en Kotlin) ne changent pas et que la valeur est connue au moment de la compilation. Pour en savoir plus sur les constantes, consultez la documentation.

  1. Maintenant que vous avez défini un prix par cupcake, créez une méthode d'assistance pour calculer le prix. Cette méthode peut être private, car elle n'est utilisée qu'au sein de cette classe. Vous allez modifier la logique de tarification pour inclure les frais de retrait le jour même.
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}

Cette ligne de code multiplie le prix par cupcake par le nombre de cupcakes commandés. Pour le code entre parenthèses, étant donné que la valeur de quantity.value peut être nulle, utilisez un opérateur Elvis (?:) . La présence de l'opérateur Elvis (?:) indique l'utilisation de l'expression de gauche si elle n'a pas une valeur nulle. Toutefois, si l'expression de gauche a une valeur nulle, c'est l'expression qui se trouve à droite de l'opérateur Elvis qui sera utilisée (0 ici).

  1. Dans la même classe OrderViewModel, modifiez la variable de prix lorsque la quantité est définie. Appelez la nouvelle fonction dans la fonction setQuantity().
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
    updatePrice()
}

Lier la propriété du prix à l'interface utilisateur

  1. Dans les mises en page pour fragment_flavor.xml, fragment_pickup.xml et fragment_summary.xml, assurez-vous que la variable de données viewModel de type com.example.cupcake.model.OrderViewModel est définie.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. Dans la méthode onViewCreated() de chaque classe de fragment, veillez à lier l'instance d'objet du ViewModel dans le fragment à la variable de données du ViewModel dans la mise en page.
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. Dans chaque mise en page de fragment, définissez le prix à l'aide de la variable viewModel si celui-ci est affiché dans la mise en page. Commencez par modifier le fichier fragment_flavor.xml. Pour l'affichage de texte subtotal, définissez la valeur de l'attribut android:text sur "@{@string/subtotal_price(viewModel.price)}".. Cette expression de liaison de données utilise la ressource de chaîne @string/subtotal_price et transmet un paramètre (le prix depuis le ViewModel), de sorte que le résultat affiche Subtotal 12.0 (Sous-total 12,0), par exemple.
...

<TextView
    android:id="@+id/subtotal"
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

Vous utilisez cette ressource de chaîne déjà déclarée dans le fichier strings.xml :

<string name="subtotal_price">Subtotal %s</string>
  1. Exécutez l'application. Si vous sélectionnez One cupcake (Un cupcake) dans le fragment de départ, celui-ci affichera Subtotal 2.0 (Sous-total 2,0). Si vous sélectionnez Six cupcakes, le fragment affichera Subtotal 12.0 (Sous-total 12,0), etc. Vous définirez la devise appropriée pour le prix dans la suite de l'atelier. Ce comportement est donc normal pour l'instant.

  1. Effectuez maintenant une modification similaire pour les fragments "pickup" et "summary". Dans les mises en page fragment_pickup.xml et fragment_summary.xml, modifiez les affichages de texte pour qu'ils utilisent également la propriété viewModel price.

fragment_pickup.xml

...

<TextView
    android:id="@+id/subtotal"
    ...
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

fragment_summary.xml

...

<TextView
   android:id="@+id/total"
   ...
   android:text="@{@string/total_price(viewModel.price)}"
   ... />

...

  1. Exécutez l'application. Assurez-vous que le prix affiché dans le récapitulatif de la commande est calculé correctement pour les quantités suivantes : 1, 6 et 12 cupcakes. Comme indiqué précédemment, il est normal que la mise en forme du prix ne soit pas correcte pour le moment (elle sera de 2,0 pour 2 $ ou de 12,0 pour 12 $).

Frais supplémentaires pour un retrait le jour même

Dans cette tâche, vous allez implémenter la deuxième règle selon laquelle le retrait le jour même ajoute 3 $ à la commande.

  1. Dans la classe OrderViewModel, définissez une nouvelle constante privée de premier niveau pour le coût du retrait le jour même.
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
  1. Dans updatePrice(), vérifiez si l'utilisateur a sélectionné le retrait le jour même. Vérifiez que la date du ViewModel (_date.value) est identique au premier élément de la liste dateOptions, qui correspond toujours à la date du jour.
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    if (dateOptions[0] == _date.value) {

    }
}
  1. Pour faciliter ces calculs, introduisez une variable temporaire, calculatedPrice. Calculez le prix actualisé et réattribuez-lui la valeur _price.value.
private fun updatePrice() {
    var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    // If the user selected the first option (today) for pickup, add the surcharge
    if (dateOptions[0] == _date.value) {
        calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
    }
    _price.value = calculatedPrice
}
  1. Appelez la méthode d'assistance updatePrice() depuis la méthode setDate() pour ajouter le coût du retrait le jour même.
fun setDate(pickupDate: String) {
    _date.value = pickupDate
    updatePrice()
}
  1. Exécutez votre application, puis parcourez-la. Vous remarquerez que la modification de la date de retrait n'entraîne pas la suppression du coût de retrait le jour même du prix total. En effet, le prix est modifié dans le ViewModel, mais il n'est pas notifié à la mise en page de la liaison.

2ea8e000fb4e6ec8.png

Définir LifecycleOwner pour observer LiveData

LifecycleOwner est une classe qui contient les informations sur l'état de cycle de vie d'un composant Android (comme une activité ou un fragment). Un observateur LiveData n'observe les modifications apportées aux données de l'application que si le propriétaire du cycle de vie est actif (STARTED ou RESUMED).

Dans votre application, l'objet LiveData ou les données observables correspondent à la propriété price du ViewModel. Les propriétaires du cycle de vie sont les fragments "flavor", "pickup" et "summary". Les observateurs LiveData sont les expressions de liaison des fichiers de mise en page qui comportent des données observables, comme le prix. Avec la liaison de données, lorsqu'une valeur observable change, les éléments d'interface utilisateur auxquels elle est associée sont mis à jour automatiquement.

Exemple d'expression de liaison : android:text="@{@string/subtotal_price(viewModel.price)}"

Pour que les éléments de l'interface utilisateur soient mis à jour automatiquement, vous devez associer binding.lifecycleOwner

avec les propriétaires du cycle de vie dans l'application. Vous implémenterez ceci par la suite.

  1. Dans les classes FlavorFragment, PickupFragment et SummaryFragment, dans la méthode onViewCreated(), ajoutez le code suivant dans le bloc binding?.apply. Le propriétaire du cycle de vie sera ainsi défini sur l'objet de liaison. En définissant le propriétaire du cycle de vie, l'application pourra observer des objets LiveData.
binding?.apply {
    lifecycleOwner = viewLifecycleOwner
    ...
}
  1. Exécutez à nouveau votre application. Sur l'écran de retrait, modifiez la date de retrait : vous remarquerez que le prix change automatiquement. Le coût de retrait de la commande s'affiche correctement sur l'écran récapitulatif.
  2. Notez que si vous sélectionnez la date de retrait en magasin le jour même, le prix de la commande augmente de 3 $. Le prix d'une date ultérieure doit toujours correspondre à la quantité de cupcakes x 2 $.

  1. Testez des quantités, des saveurs et des dates de retrait différentes. Le prix mis à jour à partir du ViewModel doit maintenant s'afficher pour chaque fragment. L'avantage, c'est que vous n'avez pas besoin d'écrire de code Kotlin supplémentaire pour que l'interface utilisateur se mette à jour à chaque changement.

f4c0a3c5ea916d03.png

Pour terminer l'implémentation de la fonctionnalité de prix, vous devez mettre en forme le prix dans la devise locale.

Modifier le format de prix avec la transformation LiveData

La ou les méthodes de transformation LiveData permettent de manipuler des données sur la source LiveData et de renvoyer un objet LiveData obtenu. En d'autres termes, elle transforme la valeur de LiveData en une autre valeur. Ces transformations ne sont pas calculées, sauf si un observateur observe l'objet LiveData.

Transformations.map() est une fonction de transformation. Cette méthode utilise la LiveData source et une fonction en tant que paramètres. La fonction manipule la source LiveData et renvoie une valeur mise à jour, qui est également observable.

Voici quelques exemples en temps réel pour lesquels vous pouvez utiliser une transformation LiveData :

  • Mettre en forme et afficher les dates et les heures
  • Trier une liste d'éléments
  • Filtrer ou regrouper les éléments
  • Calculer le résultat à partir d'une liste, comme la somme de plusieurs éléments, le nombre d'éléments, retourner au dernier élément, et ainsi de suite.

Dans cette tâche, vous allez utiliser la méthode Transformations.map() pour mettre en forme le prix dans la devise locale. Vous allez transformer le prix d'origine qui s'affiche en valeur décimale (LiveData<Double>) en une valeur de chaîne (LiveData<String>).

  1. Dans la classe OrderViewModel, définissez le type de propriété de support sur LiveData<String> au lieu de LiveData<Double>.. Le prix mis en forme sera une chaîne avec un symbole de devise comme "$". Vous corrigerez l'erreur d'initialisation à l'étape suivante.
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
  1. Utilisez Transformations.map() pour initialiser la nouvelle variable, transmettre le _price et une fonction lambda. Utilisez la méthode getCurrencyInstance() dans la classe NumberFormat pour convertir le format du prix dans la devise locale. Le code de transformation se présente comme suit :
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
   NumberFormat.getCurrencyInstance().format(it)
}

Vous devez importer androidx.lifecycle.Transformations et java.text.NumberFormat.

  1. Exécutez l'application. La chaîne de prix mise en forme doit maintenant s'afficher pour le sous-total et le total. C'est beaucoup plus pratique pour l'utilisateur !

1853bd13a07f1bc7.png

  1. Vérifiez que votre code fonctionne comme prévu. Exemples de scénarios : commandez un cupcake, puis six, puis 12. Assurez-vous que le prix se met à jour correctement sur chaque écran. La valeur doit indiquer Subtotal $2.00 (Sous-total 2 $) pour les fragments "flavor" et "pickup", et Total $2.00 (Total 2 $) pour le récapitulatif de la commande. Assurez-vous également que le récapitulatif de la commande contient les bonnes informations.

9. Configurer des écouteurs de clics à l'aide de "listener binding"

Dans cette tâche, vous allez utiliser l'expression "listener binding" pour lier les écouteurs de clics sur le bouton des classes de fragment à la mise en page.

  1. Dans le fichier de mise en page fragment_start.xml, ajoutez une variable de données appelée startFragment de type com.example.cupcake.StartFragment. Assurez-vous que le nom du package du fragment correspond au nom du package de votre application.
<layout ...>

    <data>
        <variable
            name="startFragment"
            type="com.example.cupcake.StartFragment" />
    </data>
    ...
    <ScrollView ...>
  1. Dans StartFragment.kt, dans la méthode onViewCreated(), liez la nouvelle variable de données à l'instance de fragment. Vous pouvez accéder à l'instance de fragment à l'intérieur du fragment à l'aide du mot clé this. Supprimez le bloc binding?.apply et le code qu'il contient. Une fois terminée, la méthode doit se présenter comme suit :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.startFragment = this
}
  1. Dans fragment_start.xml, ajoutez des écouteurs d'événements à l'aide d'une expression "listener binding" à l'attribut 'onClick pour les boutons, appelez orderCupcake() sur startFragment en indiquant le nombre de cupcakes.
<Button
    android:id="@+id/order_one_cupcake"
    android:onClick="@{() -> startFragment.orderCupcake(1)}"
    ... />

<Button
    android:id="@+id/order_six_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(6)}"
    ... />

<Button
    android:id="@+id/order_twelve_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(12)}"
    ... />
  1. Exécutez l'application. Notez que les gestionnaires de clics sur les boutons du startFragment fonctionnent comme prévu.
  2. De même, ajoutez la variable de données ci-dessus dans d'autres mises en page pour lier l'instance de fragment fragment_flavor.xml, fragment_pickup.xml et fragment_summary.xml.

Dans fragment_flavor.xml

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="flavorFragment"
            type="com.example.cupcake.FlavorFragment" />
    </data>

    <ScrollView ...>

Dans fragment_pickup.xml :

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="pickupFragment"
            type="com.example.cupcake.PickupFragment" />
    </data>

    <ScrollView ...>

Dans fragment_summary.xml :

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="summaryFragment"
            type="com.example.cupcake.SummaryFragment" />
    </data>

    <ScrollView ...>
  1. Dans les autres classes de fragments, supprimez le code qui définit manuellement l'écouteur de clics sur les boutons dans les méthodes onViewCreated().
  2. Dans les méthodes onViewCreated(), la variable de données de fragment est liée à l'instance de fragment. Vous allez utiliser le mot clé this différemment, car dans le bloc binding?.apply, le mot clé this fait référence à l'instance de liaison, et non à l'instance de fragment. Utilisez @ et spécifiez le nom de la classe du fragment de manière explicite, par exemple this@FlavorFragment. Une fois terminées, les méthodes onViewCreated() devraient se présenter comme suit :

La méthode onViewCreated() dans la classe FlavorFragment devrait se présenter comme suit :

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding?.apply {
        lifecycleOwner = viewLifecycleOwner
        viewModel = sharedViewModel
        flavorFragment = this@FlavorFragment
    }
}

La méthode onViewCreated() dans la classe PickupFragment devrait se présenter comme suit :

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       pickupFragment = this@PickupFragment
   }
}

La méthode onViewCreated() obtenue dans la méthode de classe SummaryFragment devrait se présenter comme suit :

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       summaryFragment = this@SummaryFragment
   }
}
  1. De même, dans les autres fichiers de mise en page, ajoutez des expressions de "listener binding" à l'attribut onClick pour les boutons.

Dans fragment_flavor.xml :

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> flavorFragment.goToNextScreen()}"
    ... />

Dans fragment_pickup.xml :

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> pickupFragment.goToNextScreen()}"
    ... />

Dans fragment_summary.xml :

<Button
    android:id="@+id/send_button"
    android:onClick="@{() -> summaryFragment.sendOrder()}"
    ...>
  1. Exécutez l'application pour vérifier que les boutons fonctionnent toujours comme prévu. Le comportement ne devrait pas changer, mais vous avez maintenant configuré des écouteurs de clics à l'aide d'expressions "listener binding".

Bravo ! Vous avez terminé cet atelier de programmation et l'application Cupcake est prête à l'emploi. Toutefois, l'application n'est pas encore terminée. Dans l'atelier de programmation suivant, vous ajouterez un bouton Annuler et modifierez la pile "Retour". Vous découvrirez également ce qu'est une pile "Retour" et d'autres sujets inédits. À bientôt !

10. Code de solution

Le code de solution de cet atelier de programmation figure dans le projet ci-dessous. Utilisez la branche viewmodel pour extraire ou télécharger le code.

Pour obtenir le code de cet atelier de programmation et l'ouvrir dans Android Studio, procédez comme suit :

Obtenir le code

  1. Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
  2. Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.

5b0a76c50478a73f.png

  1. Dans la boîte de dialogue, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
  2. Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
  3. Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.

Ouvrir le projet dans Android Studio

  1. Lancez Android Studio.
  2. Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).

36cc44fcf0f89a1d.png

Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).

21f3eec988dcfbe9.png

  1. Dans la boîte de dialogue Import Project (Importer un projet), accédez à l'emplacement du dossier du projet décompressé. Il se trouve probablement dans le dossier Téléchargements.
  2. Double-cliquez sur le dossier de ce projet.
  3. Attendez qu'Android Studio ouvre le projet.
  4. Cliquez sur le bouton Run (Exécuter) 11c34fc5e516fb1c.png pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
  5. Parcourez les fichiers du projet dans la fenêtre de l'outil Projet pour voir comment l'application est configurée.

11. Résumé

  • Le ViewModel fait partie des composants d'architecture Android, et les données de l'application enregistrées dans le ViewModel sont conservées lors des modifications de configuration. Pour ajouter un ViewModel à votre application, vous devez créer une classe basée sur la classe ViewModel.
  • Le ViewModel partagé permet d'enregistrer les données de l'application à partir de plusieurs fragments dans un seul ViewModel. Plusieurs fragments de l'application accéderont au ViewModel partagé en fonction du champ d'application de leur activité.
  • LifecycleOwner est une classe qui contient les informations sur l'état de cycle de vie d'un composant Android (comme une activité ou un fragment).
  • Un observateur LiveData n'observe les modifications apportées aux données de l'application que si le propriétaire du cycle de vie est actif (STARTED ou RESUMED).
  • Les expressions "listener binding" sont des expressions lambda qui s'exécutent lorsqu'un événement se produit, comme lors d'un événement onClick. Elles sont semblables aux références de méthodes telles que textview.setOnClickListener(clickListener), mais elles vous permettent d'exécuter des expressions de liaison de données arbitraires.
  • La ou les méthodes de transformation LiveData permettent de manipuler des données sur la source LiveData et de renvoyer un objet LiveData obtenu.
  • Les frameworks Android fournissent une classe appelée SimpleDateFormat, qui permet de mettre en forme et d'analyser les dates en tenant compte des paramètres régionaux. Elle permet de mettre en forme (date → texte) et d'analyser les dates (texte → date).

12. En savoir plus