Lire et mettre à jour des données avec Room

1. Avant de commencer

Dans les précédents ateliers de programmation, vous avez appris à utiliser une bibliothèque de persistance Room, une couche d'abstraction sur une base de données SQLite pour stocker les données de l'application. Dans cet atelier, vous ajouterez des fonctionnalités à l'application Inventory et découvrirez comment lire, afficher, mettre à jour et supprimer des données de la base de données SQLite à l'aide de Room. Vous utiliserez un RecyclerView (une vue recycleur) pour afficher les données de la base de données et les mettre automatiquement à jour lorsque les données sous-jacentes de la base de données sont modifiées.

Conditions préalables

  • Vous savez créer la base de données SQLite et interagir avec elle à l'aide de la bibliothèque Room.
  • Vous savez créer une entité, un DAO et des classes de base de données.
  • Vous savez utiliser un objet d'accès aux données (DAO, Data Access Object) pour mapper des fonctions Kotlin à des requêtes SQL.
  • Vous savez comment afficher des éléments de liste dans un RecyclerView.
  • Vous avez suivi le précédent atelier de programmation de ce module, Conserver des données avec Room.

Points abordés

  • Lire et afficher des entités à partir d'une base de données SQLite.
  • Mettre à jour et supprimer des entités d'une base de données SQLite à l'aide de la bibliothèque Room.

Objectifs de l'atelier

  • Vous allez créer une application Inventory qui affiche une liste d'éléments d'inventaire. L'application peut mettre à jour, modifier et supprimer des éléments de la base de données de l'application à l'aide de Room.

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

Cet atelier de programmation utilise le code de solution de l'application Inventory de l'atelier de programmation précédent comme code de démarrage. L'application de démarrage enregistre déjà les données à l'aide de la bibliothèque de persistance de Room. L'utilisateur peut ajouter des données à la base de données de l'application à l'aide de l'écran Ajouter un élément.

Remarque : la version actuelle de l'application de démarrage n'affiche pas la date stockée dans la base de données.

771c6a677ecd96c7.png

Dans cet atelier de programmation, vous allez étendre l'application pour lire et afficher les données, mettre à jour et supprimer des entités dans la base de données à l'aide de la bibliothèque Room.

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

Ce code de démarrage est identique au code de la solution de l'atelier de programmation précédent.

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

3. Ajouter un RecyclerView

Dans cette tâche, vous allez ajouter un RecyclerView à l'application pour afficher les données stockées dans la base de données.

Ajouter une fonction d'assistance pour formater le prix

Voici une capture d'écran de l'application finale.

d6e7b7b9f12e7a16.png

Notez que le prix est affiché au format de devise. Pour convertir une valeur double au format de devise souhaité, vous devez ajouter une fonction d'extension à la classe Item.

Fonctions d'extension

Kotlin permet d'étendre une classe avec de nouvelles fonctionnalités sans avoir à hériter de la classe ni à modifier sa définition déjà existante. Cela signifie que vous pouvez ajouter des fonctions à une classe existante sans avoir à accéder à son code source. Pour ce faire, utilisez des déclarations spéciales appelées extensions.

Par exemple, vous pouvez écrire de nouvelles fonctions pour une classe à partir d'une bibliothèque tierce que vous ne pouvez pas modifier. Ces fonctions sont disponibles pour les appels, comme s'il s'agissait de méthodes de la classe d'origine. Ces fonctions sont appelées fonctions d'extension. (Il existe également des propriétés d'extension qui vous permettent de définir de nouvelles propriétés pour les classes existantes, mais elles ne sont pas abordées dans cet atelier de programmation.)

Les fonctions d'extension ne modifient pas la classe, mais vous permettent d'utiliser la notation par points pour appeler la fonction sur les objets de cette classe.

Par exemple, dans l'extrait de code suivant, vous avez une classe nommée Square. Cette classe possède une propriété pour le côté et une fonction permettant de calculer l'aire du carré. Notez la fonction d'extension Square.perimeter() : le nom de la fonction est précédé de la classe sur laquelle elle agit. Dans la fonction, vous pouvez référencer les propriétés publiques de la classe Square.

Observez l'utilisation de la fonction d'extension dans la fonction main(). La fonction d'extension créée, perimeter(), est appelée en tant que fonction standard dans cette classe Square.

Exemple :

class Square(val side: Double){
        fun area(): Double{
        return side * side;
    }
}

// Extension function to calculate the perimeter of the square
fun Square.perimeter(): Double{
        return 4 * side;
}

// Usage
fun main(args: Array<String>){
      val square = Square(5.5);
      val perimeterValue = square.perimeter()
      println("Perimeter: $perimeterValue")
      val areaValue = square.area()
      println("Area: $areaValue")
}

Au cours de cette étape, vous allez formater le prix de l'élément en une chaîne au format de devise. En général, il n'est pas souhaitable de modifier une classe d'entités qui représente des données uniquement pour leur mise en forme (voir le principe de responsabilité unique), une fonction d'extension est donc utilisée.

  1. Dans Item.kt, sous la définition de la classe, ajoutez une fonction d'extension nommée Item.getFormattedPrice() qui ne reçoit aucun paramètre et renvoie une String. Notez que le nom de la classe et la notation par points sont utilisés dans le nom de la fonction.
fun Item.getFormattedPrice(): String =
   NumberFormat.getCurrencyInstance().format(itemPrice)

Importez java.text.NumberFormat lorsqu'Android Studio vous le demande.

Ajouter ListAdapter

Au cours de cette étape, vous allez ajouter un adaptateur de liste au RecyclerView. Étant donné que vous savez comment mettre en œuvre l'adaptateur grâce aux précédents ateliers de programmation, les instructions sont résumées ci-dessous. Le fichier ItemListAdapter terminé est disponible à la fin de cette étape, pour vous aider à mieux comprendre les concepts de Room utilisés au cours de cet atelier.

  1. Dans le package com.example.inventory, ajoutez une classe Kotlin nommée ItemListAdapter. Transmettez une fonction nommée onItemClicked() en tant que paramètre constructeur qui reçoit un objet Item en tant que paramètre.
  2. Modifiez la signature de la classe ItemListAdapter pour étendre ListAdapter. Transmettez Item et ItemListAdapter.ItemViewHolder en tant que paramètres.
  3. Ajoutez le paramètre constructeur DiffCallback. ListAdapter utilisera ces informations pour identifier ce qui a changé dans la liste.
  4. Remplacez les méthodes requises onCreateViewHolder() et onBindViewHolder().
  5. La méthode onCreateViewHolder() renvoie un nouveau ViewHolder lorsque RecyclerView en a besoin.
  6. Dans la méthode onCreateViewHolder(), créez un View, puis enrichissez-le à partir du fichier de mise en page item_list_item.xml à l'aide de ItemListItemBinding, la classe de liaison générée automatiquement.
  7. Implémentez la méthode onBindViewHolder(). Obtenez l'élément actuel en utilisant la méthode getItem() qui transmet la position.
  8. Définissez l'écouteur de clics sur itemView et appelez la fonction onItemClicked() dans l'écouteur.
  9. Définissez la classe ItemViewHolder, puis étendez-la à partir de RecyclerView.ViewHolder.. Ignorez la fonction bind() et transmettez l'objet Item.
  10. Définissez un objet compagnon. Dans l'objet compagnon, définissez une val de type DiffUtil.ItemCallback<Item>(), nommées DiffCallback. Remplacez les méthodes requises areItemsTheSame() et areContentsTheSame(), puis définissez-les.

Une fois terminée, la classe doit se présenter comme suit :

package com.example.inventory

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.inventory.data.Item
import com.example.inventory.data.getFormattedPrice
import com.example.inventory.databinding.ItemListItemBinding

/**
* [ListAdapter] implementation for the recyclerview.
*/

class ItemListAdapter(private val onItemClicked: (Item) -> Unit) :
   ListAdapter<Item, ItemListAdapter.ItemViewHolder>(DiffCallback) {

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
       return ItemViewHolder(
           ItemListItemBinding.inflate(
               LayoutInflater.from(
                   parent.context
               )
           )
       )
   }

   override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
       val current = getItem(position)
       holder.itemView.setOnClickListener {
           onItemClicked(current)
       }
       holder.bind(current)
   }

   class ItemViewHolder(private var binding: ItemListItemBinding) :
       RecyclerView.ViewHolder(binding.root) {

       fun bind(item: Item) {

       }
   }

   companion object {
       private val DiffCallback = object : DiffUtil.ItemCallback<Item>() {
           override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
               return oldItem === newItem
           }

           override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
               return oldItem.itemName == newItem.itemName
           }
       }
   }
}

Observez l'écran de la liste d'inventaire de l'application finale (celle de la solution présentée à la fin de cet atelier de programmation). Notez que chaque élément de la liste affiche le nom de l'élément d'inventaire, le prix au format de devise et le stock actuel. Au cours des étapes précédentes, vous avez utilisé le fichier de mise en page item_list_item.xml avec trois TextViews pour créer des lignes. À l'étape suivante, vous allez lier les détails de l'entité à ces TextViews.

9c416f2fbf1e5ae2.png

  1. Dans ItemListAdapter.kt, implémentez la fonction bind() dans la classe ItemViewHolder. Liez itemName de TextView à item.itemName. Obtenez le prix au format de devise à l'aide de la fonction d'extension getFormattedPrice() et associez-le au itemPrice de TextView. Convertissez la valeur quantityInStock en String et liez-la à itemQuantity de TextView. Une fois terminée, la méthode doit se présenter comme suit :
fun bind(item: Item) {
   binding.apply {
       itemName.text = item.itemName
       itemPrice.text = item.getFormattedPrice()
       itemQuantity.text = item.quantityInStock.toString()
   }
}

Lorsqu'Android Studio vous le demande, importez com.example.inventory.data.getFormattedPrice.

Utiliser ListAdapter

Dans cette tâche, vous allez mettre à jour InventoryViewModel et ItemListFragment pour afficher les détails de l'élément à l'écran à l'aide de l'adaptateur de liste que vous avez créé à l'étape précédente.

  1. Au début de la classe InventoryViewModel, créez un val nommé allItems de type LiveData<List<Item>> pour les éléments de la base de données. Ne vous souciez pas de l'erreur, vous la corrigerez rapidement.
val allItems: LiveData<List<Item>>

Importez androidx.lifecycle.LiveData lorsqu'Android Studio vous le demande.

  1. Appelez getItems() sur itemDao et assignez-le à allItems. La fonction getItems() renvoie un Flow. Pour utiliser les données en tant que valeur LiveData, utilisez la fonction asLiveData(). La définition finale doit ressembler à ceci :
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()

Importez androidx.lifecycle.asLiveData lorsqu'Android Studio vous le demande.

  1. Dans ItemListFragment, au début de la classe, déclarez une propriété immuable private nommée viewModel de type InventoryViewModel. Utilisez le délégué by pour transférer l'initialisation de la propriété à la classe activityViewModels. Transmettez le constructeur InventoryViewModelFactory.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database.itemDao()
   )
}

Importez androidx.fragment.app.activityViewModels à la demande d'Android Studio.

  1. Toujours dans ItemListFragment, faites défiler la page jusqu'à la fonction onViewCreated(). Sous l'appel à super.onViewCreated(), déclarez un val nommé adapter. Initialisez la nouvelle propriété adapter à l'aide du constructeur par défaut (ItemListAdapter{}).
  2. Liez l'adapter que vous venez de créer au recyclerView comme suit :
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
  1. Toujours dans onViewCreated(), après avoir configuré l'adaptateur, associez un observateur à allItems pour écouter les modifications des données.
  2. Dans l'observateur, appelez submitList() sur l'adapter et transmettez la nouvelle liste. Cette opération met à jour le RecyclerView avec les nouveaux éléments de la liste.
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
   items.let {
       adapter.submitList(it)
   }
}
  1. Vérifiez que la méthode onViewCreated() terminée se présente comme suit : Exécutez l'application. Notez que la liste d'inventaire s'affiche si vous avez enregistré des éléments dans la base de données de votre application. Ajoutez des éléments d'inventaire à la base de données de l'application si la liste est vide.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   val adapter = ItemListAdapter {
      }
   binding.recyclerView.adapter = adapter
   viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
       items.let {
           adapter.submitList(it)
       }
   }
   binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
   binding.floatingActionButton.setOnClickListener {
       val action = ItemListFragmentDirections.actionItemListFragmentToAddItemFragment(
           getString(R.string.add_fragment_title)
       )
       this.findNavController().navigate(action)
   }
}

9c416f2fbf1e5ae2.png

4. Afficher les détails de l'élément

Dans cette tâche, vous allez lire et afficher les détails de l'entité sur l'écran Item Details (Détails de l'article). Vous utiliserez la clé primaire (l'élément id) pour lire les détails de la base de données de l'inventaire de l'application, tels que le nom, le prix et la quantité, et les afficher sur l'écran Item Detail (Détails de l'article) à l'aide du fichier de mise en page fragment_item_detail.xml. Le fichier de mise en page fragment_item_detail.xml est prédéfini pour vous et contient trois TextViews qui affichent les détails de l'élément.

d699618f5d9437df.png

Vous allez à présent implémenter les étapes suivantes :

  • Ajoutez un gestionnaire de clics à RecyclerView pour accéder à l'écran Item Details (Détails de l'article) de l'application.
  • Dans le fragment ItemListFragment, récupérez les données de la base de données et affichez-les.
  • Liez les TextViews aux données ViewModel.

Ajouter un gestionnaire de clics

  1. Dans ItemListFragment, faites défiler la page jusqu'à la fonction onViewCreated() pour mettre à jour la définition de l'adaptateur.
  2. Ajoutez un lambda en tant que paramètre de constructeur à ItemListAdapter{}.
val adapter = ItemListAdapter {
}
  1. Dans le lambda, créez un val nommé action. Vous allez bientôt corriger l'erreur d'initialisation.
val adapter = ItemListAdapter {
    val action
}
  1. Appelez la méthode actionItemListFragmentToItemDetailFragment() sur ItemListFragmentDirections qui transmet l'élément id. Attribuez l'objet NavDirections renvoyé à action.
val adapter = ItemListAdapter {
   val action =    ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
  1. Sous la définition action, récupérez une instance NavController à l'aide de this.findNavController() et appelez navigate() sur elle pour lui transmettre action. La définition de l'adaptateur doit se présenter comme suit :
val adapter = ItemListAdapter {
   val action =   ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
   this.findNavController().navigate(action)
}
  1. Exécutez l'application. Cliquez sur un élément du RecyclerView. L'application accède à l'écran Détails de l'élément. Notez que les détails sont vides. Rien ne se passe lorsque vous appuyez sur les boutons.

196553111ee69beb.png

Au cours des étapes suivantes, vous allez afficher les détails de l'entité sur l'écran Item Details (Détails de l'article) et ajouter une fonctionnalité permettant de vendre et de supprimer des boutons.

Récupérer les détails de l'élément

Au cours de cette étape, vous allez ajouter une fonction à InventoryViewModel pour récupérer les détails des éléments de la base de données en fonction de l'élément id. À l'étape suivante, vous utiliserez cette fonction pour afficher les détails de l'entité sur l'écran Détails de l'élément.

  1. Dans InventoryViewModel, ajoutez une fonction nommée retrieveItem() qui reçoit un Int pour l'ID de l'élément et renvoie un LiveData<Item>. Vous allez bientôt corriger l'erreur d'expression renvoyée.
fun retrieveItem(id: Int): LiveData<Item> {
}
  1. Dans la nouvelle fonction, appelez getItem() sur itemDao, en transmettant le paramètre id. La fonction getItem() renvoie un Flow. Pour utiliser la valeur Flow en tant que fonction LiveData, appelez la fonction asLiveData() et utilisez-la comme retour de la fonction retrieveItem(). La fonction terminée doit se présenter comme suit :
fun retrieveItem(id: Int): LiveData<Item> {
   return itemDao.getItem(id).asLiveData()
}

Associer les données aux TextViews

Au cours de cette étape, vous allez créer une instance ViewModel dans ItemDetailFragment et lier les données ViewModel aux TextViews dans l'écran Item Details (Détails de l'article). Vous allez également joindre un observateur aux données dans ViewModel pour maintenir votre liste d'inventaire à jour à l'écran, même si les données sous-jacentes de la base de données changent.

  1. Dans ItemDetailFragment, ajoutez une propriété modifiable nommée item de type entité Item. Vous utiliserez cette propriété pour stocker des informations sur une seule entité. Cette propriété sera initialisée plus tard. Ajoutez-y le préfixe lateinit.
lateinit var item: Item

Importez com.example.inventory.data.Item lorsqu'Android Studio vous le demande.

  1. Au début de la classe ItemDetailFragment, déclarez une propriété immuable private nommée viewModel de type InventoryViewModel. Utilisez le délégué by pour transférer l'initialisation de la propriété à la classe activityViewModels. Transmettez le constructeur InventoryViewModelFactory.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database.itemDao()
   )
}

Importez androidx.fragment.app.activityViewModels, si Android Studio vous le demande.

  1. Toujours dans ItemDetailFragment, créez une fonction private nommée bind() qui reçoit une instance de l'entité Item comme paramètre et ne renvoie rien.
private fun bind(item: Item) {
}
  1. Implémentez la fonction bind(), semblable à ce que vous avez fait dans ItemListAdapter. Définissez la propriété text de TextView itemName sur item.itemName. Appelez getFormattedPrice() sur la propriété item pour mettre en forme la valeur du prix, puis définissez-la sur la propriété text de itemPricede TextView. Convertissez quantityInStock en String et définissez-la sur la propriété text de itemQuantity de TextView.
private fun bind(item: Item) {
   binding.itemName.text = item.itemName
   binding.itemPrice.text = item.getFormattedPrice()
   binding.itemCount.text = item.quantityInStock.toString()
}
  1. Mettez à jour la fonction bind() pour utiliser la fonction de champ d'application apply{} au niveau du bloc de code, comme indiqué ci-dessous.
private fun bind(item: Item) {
   binding.apply {
       itemName.text = item.itemName
       itemPrice.text = item.getFormattedPrice()
       itemCount.text = item.quantityInStock.toString()
   }
}
  1. Toujours dans ItemDetailFragment, remplacez onViewCreated().
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
}
  1. Lors de l'une des étapes précédentes, vous avez transmis un ID d'élément en tant qu'argument de navigation à ItemDetailFragment à partir de ItemListFragment. Dans onViewCreated(), sous l'appel de la fonction super-classe, créez une variable immuable nommée id. Récupérez et attribuez l'argument de navigation à cette nouvelle variable.
val id = navigationArgs.itemId
  1. Vous allez maintenant utiliser cette variable id pour récupérer les détails de l'élément. Toujours dans onViewCreated(), appelez la fonction retrieveItem() sur viewModel qui transmet l'id. Associez un observateur à la valeur renvoyée en transmettant viewLifecycleOwner et un lambda.
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
   }
  1. Dans le lambda, transmettez selectedItem en tant que paramètre contenant l'entité Item récupérée dans la base de données. Dans le corps de la fonction lambda, attribuez une valeur selectedItem à item. Appelez la fonction bind() en transmettant item. La fonction terminée doit se présenter comme suit.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   val id = navigationArgs.itemId
   viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
       item = selectedItem
       bind(item)
   }
}
  1. Exécutez l'application. Cliquez sur n'importe quel élément de la liste sur l'écran Inventory (Inventaire). L'écran Item Details (Détails de l'article) s'affiche. Notez que l'écran n'est plus vide, mais qu'il affiche les détails de l'entité extraits de la base de données d'inventaire.

  1. Appuyez sur les boutons Sell, (Vendre) Delete (Supprimer) et sur les boutons d'action flottants. Rien ne se passe ! Dans les tâches suivantes, vous implémenterez la fonctionnalité de ces boutons.

5. Implémenter la fonctionnalité de vente d'un article

Dans cette tâche, vous allez étendre les fonctionnalités de l'application et implémenter la fonctionnalité de vente. Voici un aperçu général des instructions pour cette étape.

  • Ajoutez une fonction dans ViewModel pour mettre à jour une entité.
  • Créez une méthode pour réduire la quantité et mettre à jour l'entité dans la base de données de l'application.
  • Associez un écouteur de clics au bouton Vendre.
  • Désactiver le bouton Sell (Vendre) si la quantité est nulle

Maintenant, codons !

  1. Dans InventoryViewModel, ajoutez une fonction privée nommée updateItem() qui reçoit une instance de la classe d'entité Item et qui ne renvoie rien.
private fun updateItem(item: Item) {
}
  1. Implémentez la nouvelle méthode updateItem(). Pour appeler la méthode de suspension update() à partir de la classe ItemDao, lancez une coroutine à l'aide de viewModelScope. Dans le bloc de lancement, appelez la fonction update() sur itemDao en transmettant item. Votre méthode terminée devrait ressembler à ceci.
private fun updateItem(item: Item) {
   viewModelScope.launch {
       itemDao.update(item)
   }
}
  1. Toujours dans InventoryViewModel, ajoutez une autre méthode nommée sellItem() qui reçoit une instance de la classe d'entités Item et ne renvoie rien.
fun sellItem(item: Item) {
}
  1. Dans la fonction sellItem(), ajoutez une condition if pour vérifier si la valeur de item.quantityInStock est supérieure à 0.
fun sellItem(item: Item) {
   if (item.quantityInStock > 0) {
   }
}

Dans le bloc if, vous utiliserez la fonction copy() pour la classe de données afin de mettre à jour l'entité.

Classe de données : copy()

La fonction copy() est fournie par défaut à toutes les instances de classes de données. Cette fonction permet de copier un objet afin de n'en modifier que certaines propriétés.

Prenons l'exemple de la classe User et de son instance jack, comme indiqué ci-dessous. Si vous souhaitez créer une instance en ne mettant à jour que la propriété age, son implémentation se présentera comme suit :

Exemple

// Data class
data class User(val name: String = "", val age: Int = 0)

// Data class instance
val jack = User(name = "Jack", age = 1)

// A new instance is created with its age property changed, rest of the properties unchanged.
val olderJack = jack.copy(age = 2)
  1. Revenez à la fonction sellItem() dans InventoryViewModel. Dans le bloc if, créez une propriété immuable nommée newItem. Appelez la fonction copy() sur l'instance item en transmettant la valeur quantityInStock mise à jour, ce qui diminue le stock de 1.
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
  1. En dessous de la définition de newItem, appelez la fonction updateItem() qui transmet la nouvelle entité mise à jour, à savoir newItem. La méthode terminée doit se présenter comme suit.
fun sellItem(item: Item) {
   if (item.quantityInStock > 0) {
       // Decrease the quantity by 1
       val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
       updateItem(newItem)
   }
}
  1. Pour ajouter la fonctionnalité de vente des stocks, accédez à ItemDetailFragment. Faites défiler la page jusqu'à la fin de la fonction bind(). Dans le bloc apply, définissez un écouteur de clics sur le bouton Sell (Vendre) et appelez la fonction sellItem() sur viewModel.
private fun bind(item: Item) {
binding.apply {

...
    sellItem.setOnClickListener { viewModel.sellItem(item) }
    }
}
  1. Exécutez l'application. Sur l'écran Inventory, cliquez sur un élément de la liste dont la quantité est supérieure à zéro. L'écran Détails de l'élément s'affiche. Appuyez sur le bouton Sell (Vendre). Notez que la quantité est réduite de un.

aa63ca761dc8f009.png

  1. Sur l'écran Item Details (Détails de l'article), définissez la quantité sur 0 en appuyant sans arrêt sur le bouton Sell (Vendre). (Conseil : sélectionnez une entité avec moins de stocks ou créez-en une autre avec moins de quantités.) Lorsque la quantité est égale à zéro, appuyez sur le bouton Sell (Vendre). Il n'y aura pas de changement visuel. En effet, votre fonction sellItem() vérifie si la quantité est supérieure à zéro avant de la mettre à jour.

3e099d3c55596938.png

  1. Pour optimiser l'interaction des utilisateurs avec l'application, vous pouvez désactiver le bouton Sell (Vendre) lorsqu'il n'y a pas d'article à vendre. Dans InventoryViewModel, ajoutez une fonction pour vérifier si la quantité est supérieure à 0. Nommez la fonction isStockAvailable(), qui reçoit une instance Item et renvoie un Boolean.
fun isStockAvailable(item: Item): Boolean {
   return (item.quantityInStock > 0)
}
  1. Accédez à ItemDetailFragment, faites défiler la page jusqu'à la fonction bind(). Dans le bloc "apply", appelez la fonction isStockAvailable() sur viewModel en transmettant item. Définissez la valeur renvoyée sur la propriété isEnabled du bouton Sell (Vendre). Votre code doit se présenter comme suit :
private fun bind(item: Item) {
   binding.apply {
       ...
       sellItem.isEnabled = viewModel.isStockAvailable(item)
       sellItem.setOnClickListener { viewModel.sellItem(item) }
   }
}
  1. Exécutez votre application. Notez que le bouton Sell (Vendre) est désactivé lorsque la quantité en stock est nulle. Félicitations ! Vous avez ajouté la fonctionnalité de vente d'éléments à votre application.

5e49db8451e77c2b.png

Supprimer l'entité de l'élément

Comme pour la tâche précédente, vous allez étendre les fonctionnalités de votre application en implémentant une fonctionnalité de suppression. Vous trouverez ci-dessous des instructions générales pour cette étape, qui sont beaucoup plus faciles à mettre en œuvre que la fonctionnalité de vente.

  • Ajoutez une fonction dans ViewModel pour supprimer une entité de la base de données.
  • Ajoutez une méthode dans ItemDetailFragment pour appeler la nouvelle fonction de suppression et gérer la navigation.
  • Associez un écouteur de clics au bouton Supprimer.

Continuons à coder :

  1. Dans InventoryViewModel, ajoutez une fonction nommée deleteItem(), qui reçoit une instance de la classe d'entités Item nommée item et qui ne renvoie rien. Dans la fonction deleteItem(), lancez une coroutine avec viewModelScope. Dans le bloc launch, appelez la méthode delete() sur itemDao en transmettant item.
fun deleteItem(item: Item) {
   viewModelScope.launch {
       itemDao.delete(item)
   }
}
  1. Dans ItemDetailFragment, faites défiler la page jusqu'au début de la fonction deleteItem(). Appelez deleteItem() sur viewModel et transmettez item. L'instance item contient l'entité actuellement affichée sur l'écran Item Details (Détails de l'article). Votre méthode terminée devrait ressembler à ceci.
private fun deleteItem() {
   viewModel.deleteItem(item)
   findNavController().navigateUp()
}
  1. Toujours dans ItemDetailFragment, faites défiler la page jusqu'à la fonction showConfirmationDialog(). Cette fonction vous est fournie dans le code de démarrage. Cette méthode affiche une boîte de dialogue d'alerte pour obtenir la confirmation de l'utilisateur avant de supprimer l'élément et appelle la fonction deleteItem() lorsque l'utilisateur répond par l'affirmative.
private fun showConfirmationDialog() {
        MaterialAlertDialogBuilder(requireContext())
            ...
            .setPositiveButton(getString(R.string.yes)) { _, _ ->
                deleteItem()
            }
            .show()
    }

La fonction showConfirmationDialog() affiche une boîte de dialogue d'alerte semblable à celle-ci :

728bfcbb997c8017.png

  1. Dans ItemDetailFragment, à la fin de la fonction bind(), dans le bloc apply, définissez l'écouteur de clics sur le bouton de suppression. Appelez showConfirmationDialog() dans le lambda de l'écouteur de clics.
private fun bind(item: Item) {
   binding.apply {
       ...
       deleteItem.setOnClickListener { showConfirmationDialog() }
   }
}
  1. Exécutez votre application Sélectionnez un élément de liste sur l'écran "Inventory" (Inventaire). Sur l'écran Item Details (Détails de l'article), appuyez sur le bouton Delete (Supprimer). Appuyez sur "Yes", l'application revient à l'écran "Inventaire". Notez que l'entité que vous avez supprimée ne figure plus dans la base de données de l'application. Félicitations, vous avez implémenté la fonctionnalité de suppression !

c05318ab8c216fa1.png

Modifier l'entité de l'élément

Comme pour les tâches précédentes, vous allez améliorer une autre fonctionnalité de l'application en implémentant l'entité de modification de l'élément.

Voici une rapide procédure à suivre pour modifier une entité dans la base de données de l'application :

  • Réutilisez l'écran Add Item(Ajouter un élément) en remplaçant le titre du fragment par "Edit Item" (Modifier l'élément).
  • Ajoutez un écouteur de clics au bouton d'action flottant pour accéder à l'écran Edit Item.
  • Dans les TextViews, renseignez les détails de l'entité.
  • Mettez à jour l'entité dans la base de données à l'aide de Room.

Ajouter un écouteur de clics au bouton d'action flottant

  1. Dans ItemDetailFragment, ajoutez une fonction private nommée editItem() qui ne reçoit aucun paramètre et ne renvoie rien. À l'étape suivante, vous allez réutiliser fragment_add_item.xml en remplaçant le titre de l'écran par Edit Item. Pour ce faire, vous devez envoyer la chaîne de titre du fragment avec l'ID de l'élément.
private fun editItem() {
}

Une fois que vous avez modifié le titre du fragment, l'écran Edit Item doit se présenter comme suit.

bcd407af7c515a21.png

  1. Dans la fonction editItem(), créez une variable immuable nommée action. Appelez actionItemDetailFragmentToAddItemFragment() sur ItemDetailFragmentDirections en transmettant la chaîne de titre edit_fragment_title et l'élément id. Attribuez la valeur renvoyée à action. Sous la définition de action, appelez this.findNavController().navigate() en transmettant le action pour accéder à l'écran Edit Item.
private fun editItem() {
   val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
       getString(R.string.edit_fragment_title),
       item.id
   )
   this.findNavController().navigate(action)
}
  1. Toujours dans ItemDetailFragment, faites défiler la page jusqu'à la fonction bind(). Dans le bloc apply, définissez l'écouteur de clics sur le bouton d'action flottant, appelez la fonction editItem() à partir du lambda pour accéder à l'écran Edit Item.
private fun bind(item: Item) {
   binding.apply {
       ...
       editItem.setOnClickListener { editItem() }
   }
}
  1. Exécutez l'application. Accédez à l'écran Détails de l'élément. Cliquez sur le bouton d'action flottant. Notez que le titre de l'écran est remplacé par "Edit Item", mais que tous les champs de texte sont vides. À l'étape suivante, vous allez résoudre ce problème.

a6a6583171b68230.png

Insérer des TextViews

Au cours de cette étape, vous allez renseigner les champs d'informations avec les informations des entités dans l'écran Edit Item. Puisque nous utilisons l'écran Add Item, vous allez ajouter de nouvelles fonctions au fichier Kotlin, AddItemFragment.kt.

  1. Dans AddItemFragment, ajoutez une fonction private pour lier les champs de texte avec les détails de l'entité. Nommez la fonction bind() qui reçoit l'instance de la classe d'entité "Item" et ne renvoie rien.
private fun bind(item: Item) {
}
  1. L'implémentation de la fonction bind() est très semblable à celle que vous avez effectuée précédemment dans ItemDetailFragment. Dans la fonction bind(), arrondissez le prix à la deuxième décimale à l'aide de la fonction format() et attribuez-le à un val nommé price, comme indiqué ci-dessous.
val price = "%.2f".format(item.itemPrice)
  1. Sous la définition price, utilisez la fonction de champ d'application apply sur la propriété binding, comme indiqué ci-dessous.
binding.apply {
}
  1. Dans le bloc de code de la fonction de champ d'application apply, définissez item.itemName sur la propriété de texte d'itemName. Utilisez la fonction setText() et transmettez la chaîne item.itemName et TextView.BufferType.SPANNABLE en tant que BufferType.
binding.apply {
   itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}

Importez android.widget.TextView, si Android Studio vous le demande.

  1. Comme pour l'étape ci-dessus, définissez la propriété de texte du prix EditText comme indiqué ci-dessous. Pour définir la propriété text de la quantité EditText, n'oubliez pas de convertir item.quantityInStock en String. Votre fonction terminée devrait ressembler à ceci.
private fun bind(item: Item) {
   val price = "%.2f".format(item.itemPrice)
   binding.apply {
       itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
       itemPrice.setText(price, TextView.BufferType.SPANNABLE)
       itemCount.setText(item.quantityInStock.toString(), TextView.BufferType.SPANNABLE)
   }
}
  1. Toujours dans AddItemFragment, faites défiler la page jusqu'à la fonction onViewCreated(), après l'appel de la fonction super-classe. Créez un val nommé id et récupérez itemId à partir des arguments de navigation.
val id = navigationArgs.itemId
  1. Ajoutez un bloc if-else avec une condition pour vérifier si id est supérieur à zéro et déplacez l'écouteur de clics du bouton Save (Enregistrer) dans le bloc else. Dans le bloc if, récupérez l'entité à l'aide de id et ajoutez-y un observateur. Dans l'observateur, mettez à jour la propriété item et appelez bind() en transmettant item. Vous pouvez copier et coller la fonction complète. Elle est simple et facile à comprendre. vous devez le déchiffrer par vous-même.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   val id = navigationArgs.itemId
   if (id > 0) {
       viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
           item = selectedItem
           bind(item)
       }
   } else {
       binding.saveAction.setOnClickListener {
           addNewItem()
       }
   }
}
  1. Exécutez l'application, accédez à Item Details (Détails de l'article), appuyez sur le bouton d'action flottant +. Notez que les champs contiennent les détails de l'élément. Modifiez la quantité en stock ou tout autre champ et appuyez sur le bouton "Save" (Enregistrer). Rien ne se passe ! C'est normal, vous ne mettez pas à jour l'entité dans la base de données de l'application. Vous allez bientôt résoudre ce problème.

829ceb9dd7993215.png

Mettre à jour l'entité à l'aide de Room

Dans cette dernière tâche, ajoutez les dernières parties du code pour implémenter la fonctionnalité de mise à jour. Vous allez définir les fonctions nécessaires dans ViewModel et les utiliser dans AddItemFragment.

Et c'est reparti pour coder !

  1. Dans InventoryViewModel, ajoutez une fonction private nommée getUpdatedItemEntry(), qui reçoit Int, ainsi que trois chaînes pour les détails de l'entité nommées itemName, itemPrice et itemCount. Renvoyez une instance d'Item à partir de la fonction. Le code est fourni à titre d'information.
private fun getUpdatedItemEntry(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
): Item {
}
  1. Dans la fonction getUpdatedItemEntry(), créez une instance "Item" à l'aide des paramètres de la fonction, comme indiqué ci-dessous. Renvoyez l'instance Item à partir de la fonction.
private fun getUpdatedItemEntry(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
): Item {
   return Item(
       id = itemId,
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. Toujours dans InventoryViewModel, ajoutez une autre fonction nommée updateItem(). Cette fonction reçoit également un Int et trois chaînes pour les détails de l'entité et ne renvoie rien. Utilisez les noms des variables de l'extrait de code suivant.
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
}
  1. Dans la fonction updateItem(), appelez la fonction getUpdatedItemEntry() qui transmet les informations sur l'entité, qui sont transmises en tant que paramètres de fonction, comme indiqué ci-dessous. Attribuez la valeur renvoyée à une variable immuable nommée updatedItem.
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
  1. Juste en dessous de l'appel de la fonction getUpdatedItemEntry(), appelez la fonction updateItem() qui transmet updatedItem. La fonction terminée ressemble à ceci :
fun updateItem(
   itemId: Int,
   itemName: String,
   itemPrice: String,
   itemCount: String
) {
   val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
   updateItem(updatedItem)
}
  1. Revenez à AddItemFragment, ajoutez une fonction privée nommée updateItem() sans paramètre et ne renvoyez rien. Dans la fonction, ajoutez une condition if pour valider l'entrée utilisateur en appelant la fonction isEntryValid().
private fun updateItem() {
   if (isEntryValid()) {
   }
}
  1. Dans le bloc if, appelez viewModel.updateItem() pour transmettre les détails de l'entité. Utilisez itemId à partir des arguments de navigation et des autres détails de l'entité, comme le nom, le prix et la quantité, provenant des propriétés EditTexts, comme indiqué ci-dessous.
viewModel.updateItem(
    this.navigationArgs.itemId,
    this.binding.itemName.text.toString(),
    this.binding.itemPrice.text.toString(),
    this.binding.itemCount.text.toString()
)
  1. Sous l'appel de fonction updateItem(), définissez un val nommé action. Appelez actionAddItemFragmentToItemListFragment() sur AddItemFragmentDirections, puis attribuez la valeur renvoyée à action. Accédez à ItemListFragment, appelez findNavController().navigate() en transmettant action.
private fun updateItem() {
   if (isEntryValid()) {
       viewModel.updateItem(
           this.navigationArgs.itemId,
           this.binding.itemName.text.toString(),
           this.binding.itemPrice.text.toString(),
           this.binding.itemCount.text.toString()
       )
       val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
       findNavController().navigate(action)
   }
}
  1. Toujours dans AddItemFragment, faites défiler la page jusqu'à la fonction bind(). Dans le bloc de la fonction de champ d'application binding.apply, définissez l'écouteur de clics pour le bouton Save. Appelez la fonction updateItem() dans le lambda, comme indiqué ci-dessous.
private fun bind(item: Item) {
   ...
   binding.apply {
       ...
       saveAction.setOnClickListener { updateItem() }
   }
}
  1. Exécutez l'application ! Essayez de modifier des éléments d'inventaire. Vous devriez pouvoir modifier n'importe quel élément dans la base de données de l'application Inventory.

1bbd094a77c25fc4.png

Félicitations ! Vous avez créé votre première application pour gérer les bases de données de l'application avec Room !

6. Code de solution

Le code de la solution de cet atelier de programmation se trouve dans le dépôt et la branche GitHub indiqués ci-dessous.

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

7. Résumé

  • Kotlin permet d'étendre une classe avec de nouvelles fonctionnalités sans avoir à hériter de la classe ni à modifier sa définition déjà existante. Pour ce faire, utilisez des déclarations spéciales appelées extensions.
  • Pour utiliser les données Flow en tant que valeur LiveData, utilisez la fonction asLiveData().
  • La fonction copy() est fournie par défaut à toutes les instances de classes de données. Elle vous permet de copier un objet et de n'en modifier que certaines propriétés.

8. En savoir plus

Documentation pour les développeurs Android

Références API

Références Kotlin