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.
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
- Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
- Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.
- 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.
- Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
- 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
- Lancez Android Studio.
- 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).
Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).
- 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.
- Double-cliquez sur le dossier de ce projet.
- Attendez qu'Android Studio ouvre le projet.
- Cliquez sur le bouton Exécuter pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
- 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.
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.
- Dans
Item.kt
, sous la définition de la classe, ajoutez une fonction d'extension nomméeItem.getFormattedPrice()
qui ne reçoit aucun paramètre et renvoie uneString
. 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.
- Dans le package
com.example.inventory
, ajoutez une classe Kotlin nomméeItemListAdapter
. Transmettez une fonction nomméeonItemClicked()
en tant que paramètre constructeur qui reçoit un objetItem
en tant que paramètre. - Modifiez la signature de la classe
ItemListAdapter
pour étendreListAdapter
. TransmettezItem
etItemListAdapter.ItemViewHolder
en tant que paramètres. - Ajoutez le paramètre constructeur
DiffCallback
.ListAdapter
utilisera ces informations pour identifier ce qui a changé dans la liste. - Remplacez les méthodes requises
onCreateViewHolder()
etonBindViewHolder()
. - La méthode
onCreateViewHolder()
renvoie un nouveauViewHolder
lorsque RecyclerView en a besoin. - Dans la méthode
onCreateViewHolder()
, créez unView
, puis enrichissez-le à partir du fichier de mise en pageitem_list_item.xml
à l'aide deItemListItemBinding
, la classe de liaison générée automatiquement. - Implémentez la méthode
onBindViewHolder()
. Obtenez l'élément actuel en utilisant la méthodegetItem()
qui transmet la position. - Définissez l'écouteur de clics sur
itemView
et appelez la fonctiononItemClicked()
dans l'écouteur. - Définissez la classe
ItemViewHolder
, puis étendez-la à partir deRecyclerView.ViewHolder.
. Ignorez la fonctionbind()
et transmettez l'objetItem
. - Définissez un objet compagnon. Dans l'objet compagnon, définissez une
val
de typeDiffUtil.ItemCallback<Item>()
, nomméesDiffCallback
. Remplacez les méthodes requisesareItemsTheSame()
etareContentsTheSame()
, 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.
- Dans
ItemListAdapter.kt
, implémentez la fonctionbind()
dans la classeItemViewHolder
. LiezitemName
de TextView àitem.itemName
. Obtenez le prix au format de devise à l'aide de la fonction d'extensiongetFormattedPrice()
et associez-le auitemPrice
de TextView. Convertissez la valeurquantityInStock
enString
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.
- Au début de la classe
InventoryViewModel
, créez unval
nomméallItems
de typeLiveData<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.
- Appelez
getItems()
suritemDao
et assignez-le àallItems
. La fonctiongetItems()
renvoie unFlow
. Pour utiliser les données en tant que valeurLiveData
, utilisez la fonctionasLiveData()
. 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.
- Dans
ItemListFragment
, au début de la classe, déclarez une propriété immuableprivate
nomméeviewModel
de typeInventoryViewModel
. Utilisez le déléguéby
pour transférer l'initialisation de la propriété à la classeactivityViewModels
. Transmettez le constructeurInventoryViewModelFactory
.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
Importez androidx.fragment.app.activityViewModels
à la demande d'Android Studio.
- Toujours dans
ItemListFragment
, faites défiler la page jusqu'à la fonctiononViewCreated()
. Sous l'appel àsuper.onViewCreated()
, déclarez unval
nomméadapter
. Initialisez la nouvelle propriétéadapter
à l'aide du constructeur par défaut (ItemListAdapter{}
). - Liez l'
adapter
que vous venez de créer aurecyclerView
comme suit :
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
- Toujours dans
onViewCreated()
, après avoir configuré l'adaptateur, associez un observateur àallItems
pour écouter les modifications des données. - 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)
}
}
- 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)
}
}
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.
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
- Dans
ItemListFragment
, faites défiler la page jusqu'à la fonctiononViewCreated()
pour mettre à jour la définition de l'adaptateur. - Ajoutez un lambda en tant que paramètre de constructeur à
ItemListAdapter{}
.
val adapter = ItemListAdapter {
}
- Dans le lambda, créez un
val
nomméaction
. Vous allez bientôt corriger l'erreur d'initialisation.
val adapter = ItemListAdapter {
val action
}
- Appelez la méthode
actionItemListFragmentToItemDetailFragment()
surItemListFragmentDirections
qui transmet l'élémentid
. Attribuez l'objetNavDirections
renvoyé àaction
.
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
- Sous la définition
action
, récupérez une instanceNavController
à l'aide dethis.
findNavController
()
et appeleznavigate()
sur elle pour lui transmettreaction
. 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)
}
- 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.
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.
- Dans
InventoryViewModel
, ajoutez une fonction nomméeretrieveItem()
qui reçoit unInt
pour l'ID de l'élément et renvoie unLiveData<Item>
. Vous allez bientôt corriger l'erreur d'expression renvoyée.
fun retrieveItem(id: Int): LiveData<Item> {
}
- Dans la nouvelle fonction, appelez
getItem()
suritemDao
, en transmettant le paramètreid
. La fonctiongetItem()
renvoie unFlow
. Pour utiliser la valeurFlow
en tant que fonctionLiveData
, appelez la fonctionasLiveData()
et utilisez-la comme retour de la fonctionretrieveItem()
. 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.
- Dans
ItemDetailFragment
, ajoutez une propriété modifiable nomméeitem
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éfixelateinit
.
lateinit var item: Item
Importez com.example.inventory.data.Item
lorsqu'Android Studio vous le demande.
- Au début de la classe
ItemDetailFragment
, déclarez une propriété immuableprivate
nomméeviewModel
de typeInventoryViewModel
. Utilisez le déléguéby
pour transférer l'initialisation de la propriété à la classeactivityViewModels
. Transmettez le constructeurInventoryViewModelFactory
.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
Importez androidx.fragment.app.activityViewModels
, si Android Studio vous le demande.
- Toujours dans
ItemDetailFragment
, créez une fonctionprivate
nomméebind()
qui reçoit une instance de l'entitéItem
comme paramètre et ne renvoie rien.
private fun bind(item: Item) {
}
- Implémentez la fonction
bind()
, semblable à ce que vous avez fait dansItemListAdapter
. Définissez la propriététext
de TextViewitemName
suritem.itemName
. AppelezgetFormattedPrice
()
sur la propriétéitem
pour mettre en forme la valeur du prix, puis définissez-la sur la propriététext
deitemPrice
de TextView. ConvertissezquantityInStock
enString
et définissez-la sur la propriététext
deitemQuantity
de TextView.
private fun bind(item: Item) {
binding.itemName.text = item.itemName
binding.itemPrice.text = item.getFormattedPrice()
binding.itemCount.text = item.quantityInStock.toString()
}
- Mettez à jour la fonction
bind()
pour utiliser la fonction de champ d'applicationapply{}
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()
}
}
- Toujours dans
ItemDetailFragment
, remplacezonViewCreated()
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
- 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 deItemListFragment
. DansonViewCreated()
, sous l'appel de la fonction super-classe, créez une variable immuable nomméeid
. Récupérez et attribuez l'argument de navigation à cette nouvelle variable.
val id = navigationArgs.itemId
- Vous allez maintenant utiliser cette variable
id
pour récupérer les détails de l'élément. Toujours dansonViewCreated()
, appelez la fonctionretrieveItem()
surviewModel
qui transmet l'id
. Associez un observateur à la valeur renvoyée en transmettantviewLifecycleOwner
et un lambda.
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
}
- 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 valeurselectedItem
àitem
. Appelez la fonctionbind()
en transmettantitem
. 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)
}
}
- 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.
- 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 !
- Dans
InventoryViewModel
, ajoutez une fonction privée nomméeupdateItem()
qui reçoit une instance de la classe d'entitéItem
et qui ne renvoie rien.
private fun updateItem(item: Item) {
}
- Implémentez la nouvelle méthode
updateItem()
. Pour appeler la méthode de suspensionupdate()
à partir de la classeItemDao
, lancez une coroutine à l'aide deviewModelScope
. Dans le bloc de lancement, appelez la fonctionupdate()
suritemDao
en transmettantitem
. Votre méthode terminée devrait ressembler à ceci.
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
- Toujours dans
InventoryViewModel
, ajoutez une autre méthode nomméesellItem()
qui reçoit une instance de la classe d'entitésItem
et ne renvoie rien.
fun sellItem(item: Item) {
}
- Dans la fonction
sellItem()
, ajoutez une conditionif
pour vérifier si la valeur deitem.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)
- Revenez à la fonction
sellItem()
dansInventoryViewModel
. Dans le blocif
, créez une propriété immuable nomméenewItem
. Appelez la fonctioncopy()
sur l'instanceitem
en transmettant la valeurquantityInStock
mise à jour, ce qui diminue le stock de1
.
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
- En dessous de la définition de
newItem
, appelez la fonctionupdateItem()
qui transmet la nouvelle entité mise à jour, à savoirnewItem
. 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)
}
}
- Pour ajouter la fonctionnalité de vente des stocks, accédez à
ItemDetailFragment
. Faites défiler la page jusqu'à la fin de la fonctionbind()
. Dans le blocapply
, définissez un écouteur de clics sur le bouton Sell (Vendre) et appelez la fonctionsellItem()
surviewModel
.
private fun bind(item: Item) {
binding.apply {
...
sellItem.setOnClickListener { viewModel.sellItem(item) }
}
}
- 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.
- 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.
- 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 fonctionisStockAvailable()
, qui reçoit une instanceItem
et renvoie unBoolean
.
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
- Accédez à
ItemDetailFragment
, faites défiler la page jusqu'à la fonctionbind()
. Dans le bloc "apply", appelez la fonctionisStockAvailable()
surviewModel
en transmettantitem
. 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) }
}
}
- 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.
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 :
- Dans
InventoryViewModel
, ajoutez une fonction nomméedeleteItem()
, qui reçoit une instance de la classe d'entitésItem
nomméeitem
et qui ne renvoie rien. Dans la fonctiondeleteItem()
, lancez une coroutine avecviewModelScope
. Dans le bloclaunch
, appelez la méthodedelete()
suritemDao
en transmettantitem
.
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(item)
}
}
- Dans
ItemDetailFragment
, faites défiler la page jusqu'au début de la fonctiondeleteItem()
. AppelezdeleteItem()
surviewModel
et transmettezitem
. L'instanceitem
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()
}
- Toujours dans
ItemDetailFragment
, faites défiler la page jusqu'à la fonctionshowConfirmationDialog()
. 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 fonctiondeleteItem()
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 :
- Dans
ItemDetailFragment
, à la fin de la fonctionbind()
, dans le blocapply
, définissez l'écouteur de clics sur le bouton de suppression. AppelezshowConfirmationDialog()
dans le lambda de l'écouteur de clics.
private fun bind(item: Item) {
binding.apply {
...
deleteItem.setOnClickListener { showConfirmationDialog() }
}
}
- 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 !
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
- Dans
ItemDetailFragment
, ajoutez une fonctionprivate
nomméeeditItem()
qui ne reçoit aucun paramètre et ne renvoie rien. À l'étape suivante, vous allez réutiliserfragment_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.
- Dans la fonction
editItem()
, créez une variable immuable nomméeaction
. AppelezactionItemDetailFragmentToAddItemFragment()
surItemDetailFragmentDirections
en transmettant la chaîne de titreedit_fragment_title
et l'élémentid
. Attribuez la valeur renvoyée àaction
. Sous la définition deaction
, appelezthis.findNavController().navigate()
en transmettant leaction
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)
}
- Toujours dans
ItemDetailFragment
, faites défiler la page jusqu'à la fonctionbind()
. Dans le blocapply
, définissez l'écouteur de clics sur le bouton d'action flottant, appelez la fonctioneditItem()
à partir du lambda pour accéder à l'écran Edit Item.
private fun bind(item: Item) {
binding.apply {
...
editItem.setOnClickListener { editItem() }
}
}
- 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.
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
.
- Dans
AddItemFragment
, ajoutez une fonctionprivate
pour lier les champs de texte avec les détails de l'entité. Nommez la fonctionbind()
qui reçoit l'instance de la classe d'entité "Item" et ne renvoie rien.
private fun bind(item: Item) {
}
- L'implémentation de la fonction
bind()
est très semblable à celle que vous avez effectuée précédemment dansItemDetailFragment
. Dans la fonctionbind()
, arrondissez le prix à la deuxième décimale à l'aide de la fonctionformat()
et attribuez-le à unval
nomméprice
, comme indiqué ci-dessous.
val price = "%.2f".format(item.itemPrice)
- Sous la définition
price
, utilisez la fonction de champ d'applicationapply
sur la propriétébinding
, comme indiqué ci-dessous.
binding.apply {
}
- Dans le bloc de code de la fonction de champ d'application
apply
, définissezitem.itemName
sur la propriété de texte d'itemName
. Utilisez la fonctionsetText()
et transmettez la chaîneitem.itemName
etTextView.BufferType.SPANNABLE
en tant queBufferType
.
binding.apply {
itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}
Importez android.widget.TextView
, si Android Studio vous le demande.
- 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 convertiritem.quantityInStock
enString
. 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)
}
}
- Toujours dans
AddItemFragment
, faites défiler la page jusqu'à la fonctiononViewCreated()
, après l'appel de la fonction super-classe. Créez unval
nomméid
et récupérezitemId
à partir des arguments de navigation.
val id = navigationArgs.itemId
- Ajoutez un bloc
if-else
avec une condition pour vérifier siid
est supérieur à zéro et déplacez l'écouteur de clics du bouton Save (Enregistrer) dans le blocelse
. Dans le blocif
, récupérez l'entité à l'aide deid
et ajoutez-y un observateur. Dans l'observateur, mettez à jour la propriétéitem
et appelezbind()
en transmettantitem
. 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()
}
}
}
- 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.
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 !
- Dans
InventoryViewModel
, ajoutez une fonctionprivate
nomméegetUpdatedItemEntry()
, qui reçoitInt
, ainsi que trois chaînes pour les détails de l'entité nomméesitemName
,itemPrice
etitemCount
. 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 {
}
- Dans la fonction
getUpdatedItemEntry()
, créez une instance "Item" à l'aide des paramètres de la fonction, comme indiqué ci-dessous. Renvoyez l'instanceItem
à 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()
)
}
- Toujours dans
InventoryViewModel
, ajoutez une autre fonction nomméeupdateItem()
. Cette fonction reçoit également unInt
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
) {
}
- Dans la fonction
updateItem()
, appelez la fonctiongetUpdatedItemEntry()
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éeupdatedItem
.
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
- Juste en dessous de l'appel de la fonction
getUpdatedItemEntry()
, appelez la fonctionupdateItem()
qui transmetupdatedItem
. 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)
}
- Revenez à
AddItemFragment
, ajoutez une fonction privée nomméeupdateItem()
sans paramètre et ne renvoyez rien. Dans la fonction, ajoutez une conditionif
pour valider l'entrée utilisateur en appelant la fonctionisEntryValid()
.
private fun updateItem() {
if (isEntryValid()) {
}
}
- Dans le bloc
if
, appelezviewModel.updateItem()
pour transmettre les détails de l'entité. UtilisezitemId
à 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()
)
- Sous l'appel de fonction
updateItem()
, définissez unval
nomméaction
. AppelezactionAddItemFragmentToItemListFragment()
surAddItemFragmentDirections
, puis attribuez la valeur renvoyée àaction
. Accédez àItemListFragment
, appelezfindNavController().navigate()
en transmettantaction
.
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)
}
}
- Toujours dans
AddItemFragment
, faites défiler la page jusqu'à la fonctionbind()
. Dans le bloc de la fonction de champ d'applicationbinding.
apply
, définissez l'écouteur de clics pour le bouton Save. Appelez la fonctionupdateItem()
dans le lambda, comme indiqué ci-dessous.
private fun bind(item: Item) {
...
binding.apply {
...
saveAction.setOnClickListener { updateItem() }
}
}
- 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.
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
- Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
- Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.
- 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.
- Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
- 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
- Lancez Android Studio.
- 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).
Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).
- 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.
- Double-cliquez sur le dossier de ce projet.
- Attendez qu'Android Studio ouvre le projet.
- Cliquez sur le bouton Exécuter pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
- 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 valeurLiveData
, utilisez la fonctionasLiveData()
. - 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
- Transmettre des données entre les destinations
- Chaîne Android
- Formateur Android
- Déboguer votre base de données avec l'outil d'inspection de bases de données
- Enregistrer des données dans une base de données locale à l'aide de Room
Références API
Références Kotlin