Persistance des données avec Room

1. Avant de commencer

La plupart des applications de qualité en production reposent sur des données qui doivent être enregistrées, même après que l'utilisateur a fermé l'application. Par exemple, l'application peut stocker une playlist musicale, une liste de tâches, des tickets de caisse ou des notes de frais, un catalogue de constellations ou un historique de données personnelles. Dans la plupart des cas, pour stocker ces données persistantes, vous avez besoin d'une base de données.

Room est une bibliothèque de persistance qui fait partie d'Android Jetpack. Il s'agit d'une couche d'abstraction qui s'ajoute à une base de données SQLite. SQLite utilise un langage spécialisé (SQL) pour effectuer des opérations sur la base de données. Au lieu d'utiliser directement SQLite, Room simplifie la configuration et les interactions. Room permet également de contrôler le temps de compilation des instructions SQLite.

L'image ci-dessous montre comment cette bibliothèque s'intègre à l'architecture globale recommandée dans cette formation.

7521165e051cc0d4.png

Conditions préalables

  • Vous savez comment créer une interface utilisateur de base pour une application Android.
  • Vous savez utiliser les activités, les fragments et les vues.
  • Vous savez naviguer entre les fragments et utiliser Safe Args pour transmettre des données entre fragments.
  • Vous maîtrisez les composants d'architecture Android ViewModel, LiveData et Flow, et vous savez comment utiliser ViewModelProvider.Factory pour instancier les éléments ViewModel.
  • Vous maîtrisez les principes de base de la simultanéité.
  • Vous êtes capable d'utiliser des coroutines pour des tâches de longue durée.
  • Vous disposez de connaissances fondamentales sur les bases de données SQL et le langage SQLite.

Points abordés

  • Création d'une base de données SQLite et interaction à l'aide de la bibliothèque Room
  • Création d'une entité, d'un DAO et de classes de base de données
  • Utilisation d'un objet d'accès aux données (DAO, Data Access Object) pour mapper des fonctions Kotlin à des requêtes SQL

Objectifs de l'atelier

  • Vous allez créer l'application Inventory, dont le but est d'enregistrer des éléments d'inventaire dans une base de données SQLite.

Ce dont vous avez besoin

  • Le code de démarrage de l'application Inventory
  • Un ordinateur sur lequel est installé Android Studio

2. Présentation de l'application

Dans cet atelier de programmation, vous allez utiliser une application de démarrage appelée "Inventory" et y ajouter une couche de base de données à l'aide de la bibliothèque Room. La version finale de l'application affiche une liste d'éléments de la base de données d'inventaire à l'aide d'un RecyclerView. L'utilisateur pourra ajouter, supprimer ou modifier des éléments de la base de données d'inventaire (vous compléterez les fonctionnalités de l'application dans l'atelier de programmation suivant).

Vous trouverez ci-dessous des captures d'écran de la version finale de l'application.

439ad9a8183278c5.png

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

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 peut contenir du code que vous avez déjà vu dans les ateliers de programmation précédents, ainsi que du code inconnu qui sera présenté dans les ateliers suivants.

Si vous utilisez le code de démarrage de GitHub, notez que le nom du dossier est android-basics-kotlin-inventory-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é (qui 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.

Présentation du code de démarrage

  1. Dans Android Studio, ouvrez le projet contenant le code de démarrage.
  2. Exécutez l'application sur un appareil Android ou sur un émulateur. L'émulateur ou l'appareil connecté doit utiliser le niveau d'API 26 ou supérieur. L'outil d'inspection de bases de données fonctionne mieux sur les émulateurs ou appareils utilisant ce niveau d'API.
  3. L'application n'affiche aucune donnée d'inventaire. Vous pouvez voir un bouton d'action flottant : il permet d'ajouter des éléments à la base de données.
  4. Cliquez sur le bouton d'action flottant. L'application ouvre un nouvel écran dans lequel vous pouvez saisir les détails du nouvel élément.

9c5e361a89453821.png

Problèmes avec le code de démarrage

  1. Sur l'écran Ajouter un élément, saisissez les détails d'un nouvel élément. Appuyez sur Enregistrer. Le fragment d'ajout d'élément ne se ferme pas. Revenez à l'écran précédent à l'aide de la touche de retour arrière du système. Le nouvel élément n'est pas enregistré et ne figure pas sur l'écran d'inventaire. Notez que l'application est incomplète et que le bouton Enregistrer n'est pas implémenté.

f0931dab5089a14f.png

Dans cet atelier de programmation, vous allez ajouter le code qui permet à l'application d'enregistrer les détails de l'inventaire dans la base de données SQLite. Pour interagir avec la base de données SQLite, vous utiliserez la bibliothèque de persistance Room.

Tutoriel du code

Le code de démarrage que vous avez téléchargé dispose déjà d'une mise en page. Dans ce parcours, vous allez vous concentrer sur la mise en œuvre de la logique de base de données. Voici une présentation rapide de certains fichiers pour vous aider à commencer.

main_activity.xml

Activité principale qui héberge tous les autres fragments de l'application. La méthode onCreate() récupère NavController à partir de NavHostFragment et configure la barre d'action à utiliser avec NavController.

item_list_fragment.xml

Le premier écran affiché dans l'application. Il contient principalement un RecyclerView et un bouton d'action flottant. Vous implémenterez le RecyclerView plus tard dans ce parcours.

fragment_add_item.xml

Cette mise en page contient des champs de texte permettant de saisir les détails du nouvel élément d'inventaire à ajouter.

ItemListFragment.kt

Ce fragment contient principalement du code récurrent. Dans la méthode onViewCreated(), l'écouteur de clics est défini sur le bouton d'action flottant qui permet d'accéder au fragment d'ajout d'élément.

AddItemFragment.kt

Ce fragment permet d'ajouter des éléments à la base de données. La fonction onCreateView() initialise la variable de liaison tandis que onDestroyView() masque le clavier avant de détruire le fragment.

4. Principaux composants de Room

Kotlin permet de gérer facilement les données en les organisant en classes. Ces données sont accessibles, et parfois modifiées, à l'aide d'appels de fonction. Toutefois, dans l'univers des bases de données, vous avez besoin de tables et de requêtes pour accéder aux données et les modifier. Les composants de Room que nous allons présenter ici simplifient ces workflows.

Room repose sur trois composants principaux :

  • Les entités de données représentent les tables de la base de données de votre application. Elles permettent de mettre à jour les données stockées en lignes dans ces tables, mais aussi de créer des lignes.
  • Les objets d'accès aux données (DAO) fournissent des méthodes permettant à votre application de récupérer, de modifier, d'insérer et de supprimer des données dans la base de données.
  • La classe Database contient la base de données et constitue le point d'accès principal de la connexion sous-jacente à la base de données de votre application. Cette classe fournit à votre application des instances des DAO associés à cette base de données.

Vous découvrirez et implémenterez ces composants dans la suite de cet atelier de programmation. Le schéma suivant montre comment les composants de Room travaillent ensemble pour interagir avec la base de données.

33a193a68c9a8e0e.png

Ajouter des bibliothèques Room

Dans cette tâche, vous allez ajouter à vos fichiers Gradle les bibliothèques de composants Room dont vous allez avoir besoin.

  1. Ouvrez le fichier Gradle au niveau du module, build.gradle (Module: InventoryApp.app). Dans le bloc dependencies, ajoutez les dépendances suivantes pour la bibliothèque Room.
    // Room
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

5. Créer une entité d'article

Une classe d'entité définit une table, et chaque instance de cette classe représente une ligne dans la table de la base de données. La classe d'entité dispose de mappages permettant à Room de présenter les informations de la base de données et d'interagir avec elles. Dans votre application, l'entité va contenir des informations sur les éléments d'inventaire tels que leur nom, leur prix et le stock disponible.

8c9f1659ee82ca43.png

L'annotation @Entity caractérise une classe d'entité de base de données. Pour contenir les éléments, une table de base de données est créée pour chaque classe d'entité. Chaque champ de l'entité est représenté sous la forme d'une colonne dans la base de données, sauf indication contraire. Pour en savoir plus, consultez la documentation portant sur les entités. Chaque instance d'entité stockée dans la base de données doit avoir une clé primaire. La clé primaire permet d'identifier de manière unique chaque enregistrement/entrée de vos tables de base de données. Une fois attribuée, la clé primaire ne peut plus être modifiée. Elle représente l'objet de l'entité, tant qu'il existe dans la base de données.

Dans cette tâche, vous allez créer une classe d'entité. Définissez des champs pour stocker les informations d'inventaire suivantes pour chaque élément.

  • Une valeur Int (nombre entier) pour stocker la clé primaire
  • Une valeur String (chaîne de caractères) pour stocker le nom de l'élément
  • Une valeur double (nombre décimal) pour stocker le prix de l'élément
  • Une valeur Int (nombre entier) pour stocker la quantité en stock
  1. Ouvrez le code de démarrage dans Android Studio.
  2. Créez un package appelé data sous le package de base com.example.inventory.

be39b42484ba2664.png

  1. Dans le package data, créez une classe Kotlin appelée Item. Cette classe représentera une entité de base de données dans votre application. À l'étape suivante, vous ajouterez les champs correspondants pour stocker les informations sur l'inventaire.
  2. Mettez à jour la définition de la classe Item avec le code suivant. Déclarez id de type Int, itemName de type String, itemPrice de type Double et quantityInStock de type Int comme paramètres pour le constructeur principal. Attribuez la valeur par défaut 0 à id. Il s'agit de la clé primaire, une valeur permettant d'identifier de manière unique chaque enregistrement/entrée de votre table Item.
class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)

Classes de données

Les classes de données sont principalement utilisées pour stocker des données dans Kotlin. On peut les identifier par le mot clé data. Les objets de classe de données Kotlin présentent plusieurs avantages. Le compilateur génère automatiquement des utilitaires de comparaison, d'affichage et de copie tels que toString(), copy() et equals().

Exemple :

// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}

Pour garantir la cohérence et le bon fonctionnement du code généré, les classes de données doivent répondre aux exigences suivantes :

  • Le constructeur principal doit comporter au moins un paramètre.
  • Tous les paramètres du constructeur principal doivent être marqués comme val ou var.
  • Les classes de données ne peuvent pas être abstract, open, sealed ni inner.

Pour en savoir plus sur les classes de données, consultez la documentation.

  1. Convertissez la classe Item en classe de données en ajoutant le préfixe data à sa définition.
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
  1. Au-dessus de la déclaration de la classe Item, annotez la classe de données avec @Entity. Utilisez l'argument tableName pour définir item comme nom de table SQLite.
@Entity(tableName = "item")
data class Item(
   ...
)
  1. Pour identifier la propriété id comme clé primaire, annotez-la avec @PrimaryKey. Définissez le paramètre autoGenerate sur true pour que Room génère l'ID de chaque entité. L'identifiant de chaque élément est ainsi forcément unique.
@Entity(tableName = "item")
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   ...
)
  1. Annotez les propriétés restantes avec @ColumnInfo. L'annotation ColumnInfo permet de personnaliser la colonne associée au champ spécifique. Par exemple, lorsque vous utilisez l'argument name, vous pouvez spécifier un nom de colonne différent pour le champ plutôt que d'utiliser le nom de la variable. Personnalisez les noms de propriété à l'aide de paramètres, comme indiqué ci-dessous. Cette approche est semblable à l'utilisation de tableName pour spécifier un nom différent à la base de données.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

6. Créer l'élément DAO

Objet d'accès aux données (DAO)

L'objet d'accès aux données (DAO, Data Access Object) est un modèle qui permet de séparer la couche de persistance du reste de l'application en fournissant une interface abstraite. Cette isolation suit le principe de responsabilité unique, que vous avez appris au cours des ateliers de programmation précédents.

Le DAO a pour but de masquer toutes les complexités liées à l'exécution des opérations de base de données à l'aide d'une couche de persistance sous-jacente, séparée du reste de l'application. Cela permet de modifier la couche d'accès aux données indépendamment du code qui utilise ces données.

7a8480711f04b3ef.png

Dans cette tâche, vous allez définir un objet d'accès aux données (DAO) pour Room. Les DAO sont les principaux composants de Room. Ils définissent l'interface qui accède à la base de données.

Le DAO que vous allez créer sera une interface personnalisée offrant des méthodes pratiques pour interroger (récupérer), insérer, supprimer et modifier des éléments de la base de données. Room générera une implémentation de cette classe au moment de la compilation.

Pour les opérations de base de données courantes, la bibliothèque Room fournit des annotations pratiques, telles que @Insert, @Delete et @Update. Pour tout le reste, il y a l'annotation @Query. Vous pouvez écrire n'importe quelle requête prise en charge par SQLite.

En outre, lorsque vous écrivez vos requêtes SQL dans Android Studio, le compilateur vérifie si une erreur de syntaxe est présente.

Pour l'application Inventory, vous devez être en mesure d'effectuer les opérations suivantes :

  • Insérer ou ajouter un élément.
  • Modifier un élément existant : son nom, son prix et sa quantité.
  • Obtenir un élément spécifique en fonction de sa clé primaire, id.
  • Obtenir tous les éléments afin de pouvoir les afficher.
  • Supprimer une entrée de la base de données.

bb381857d5fba511.png

Maintenant, implémentez l'élément DAO dans votre application :

  1. Dans le package data, créez la classe Kotlin ItemDao.kt.
  2. Remplacez la définition de classe par interface et ajoutez l'annotation @Dao.
@Dao
interface ItemDao {
}
  1. Dans le corps de l'interface, ajoutez une annotation @Insert. En dessous celle-ci, ajoutez une fonction insert() qui reçoit une instance de la classe Entity appelée item comme argument. Les opérations sur la base de données peuvent prendre beaucoup de temps. Elles doivent donc s'exécuter sur un thread distinct. Transformez cette fonction une fonction de suspension afin qu'elle puisse être appelée à partir d'une coroutine.
@Insert
suspend fun insert(item: Item)
  1. Ajoutez un argument OnConflict et attribuez-lui la valeur OnConflictStrategy.IGNORE. L'argument OnConflict indique à Room la marche à suivre en cas de conflit. La stratégie OnConflictStrategy.IGNORE ignore le nouvel élément si sa clé primaire figure déjà dans la base de données. Pour en savoir plus sur les stratégies de gestion de conflit disponibles, consultez la documentation.
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

Room va maintenant générer tout le code nécessaire pour insérer l'élément item dans la base de données. Lorsque vous appelez insert() à partir de votre code Kotlin, Room exécute une requête SQL pour insérer l'entité dans la base de données. Souvenez-vous que vous pouvez attribuer le nom de votre choix à la fonction. Elle ne doit pas nécessairement s'appeler insert().

  1. Ajoutez une annotation @Update avec une fonction update() pour un élément item. L'entité mise à jour a la même clé que l'entité transmise. Vous pouvez mettre à jour tout ou partie des autres propriétés de l'entité. Comme pour la méthode insert(), utilisez suspend (suspension) sur la méthode update() suivante.
@Update
suspend fun update(item: Item)
  1. Ajoutez l'annotation @Delete avec une fonction delete() pour supprimer un ou des éléments. Faites-en une méthode de suspension. L'annotation @Delete supprime un élément ou une liste d'éléments. Gardez à l'esprit que vous devez transmettre la ou les entités à supprimer. Si vous ne disposez pas de ces entités, vous devrez peut-être les récupérer avant d'appeler la fonction delete().
@Delete
suspend fun delete(item: Item)

Il n'existe aucune annotation spécifiquement adaptée à la fonctionnalité restante. Vous devez donc utiliser l'annotation @Query et fournir des requêtes SQLite.

  1. Écrivez une requête SQLite pour récupérer un élément spécifique de la table en fonction de l'id donné. Vous ajouterez ensuite une annotation Room et utiliserez une version modifiée de la requête ci-dessous aux étapes suivantes. Dans la suite de cet atelier de programmation, vous allez également remplacer cette méthode par un DAO à l'aide de Room.
  2. Sélectionnez toutes les colonnes de item.
  3. Avec WHERE, spécifiez un id qui correspond à une valeur spécifique.

Exemple :

SELECT * from item WHERE id = 1
  1. Modifiez la requête SQL ci-dessus pour y ajouter une annotation Room et un argument. Ajoutez l'annotation @Query et indiquez-y la requête en tant que paramètre de chaîne. Ajoutez un paramètre String à @Query. Il s'agit d'une requête SQLite permettant de récupérer un élément dans la table.
  2. Sélectionnez toutes les colonnes de item.
  3. Avec WHERE, l'id correspond à l'argument :id. Remarquez bien :id. Utilisez le caractère deux-points, ce qui permet à la requête de faire référence à des arguments dans la fonction.
@Query("SELECT * from item WHERE id = :id")
  1. Sous l'annotation @Query, ajoutez une fonction getItem() qui reçoit un argument Int et renvoie Flow<Item>.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>

Utiliser Flow ou LiveData comme type renvoyé vous permettra d'être averti à chaque fois que des éléments de la base de données sont modifiés. Il est recommandé d'utiliser Flow dans la couche de persistance. Room maintient ce Flow à jour pour vous, ce qui signifie que vous n'avez besoin d'obtenir les données de façon explicite qu'une seule fois. Ce système est particulièrement utile pour mettre à jour la liste d'inventaire, que vous implémenterez dans l'atelier de programmation suivant. Étant donné que le type renvoyé est Flow, Room exécute également la requête sur le thread d'arrière-plan. Vous n'avez pas besoin de la définir explicitement comme une fonction suspend et de l'appeler dans le cadre d'une coroutine.

Il est possible que vous ayez besoin d'importer Flow depuis kotlinx.coroutines.flow.Flow.

  1. Ajoutez une annotation @Query avec une fonction getItems() :
  2. Demandez à la requête SQLite de renvoyer toutes les colonnes de la table item, puis d'effectuer un tri par ordre croissant.
  3. Demandez à getItems() de renvoyer la liste des entités Item en tant que Flow. Room maintient ce Flow à jour pour vous, ce qui signifie que vous n'avez besoin d'obtenir les données de façon explicite qu'une seule fois.
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
  1. Bien qu'aucune modification ne soit visible, exécutez votre application pour vous assurer qu'elle ne comporte pas d'erreurs.

7. Créer une instance de base de données

Dans cette tâche, vous allez créer RoomDatabase, qui utilise les Entity et les DAO créés au cours de la tâche précédente. La classe de base de données définit la liste des entités et des objets d'accès aux données. Il s'agit également du point d'accès principal de la connexion sous-jacente.

La classe Database fournit à votre application les instances des DAO que vous avez définis. L'application peut ensuite utiliser ces DAO pour récupérer des données à partir de la base de données en tant qu'instances des objets d'entité associés. L'application peut également utiliser les entités de données définies pour modifier des lignes dans les tables correspondantes ou pour en créer.

Vous devez créer une classe RoomDatabase abstraite, annotée avec @Database. Cette classe comporte une méthode qui crée une instance de RoomDatabase si elle n'existe pas ou qui renvoie l'instance existante le cas échéant.

Voici le processus général pour obtenir l'instance RoomDatabase :

  • Créez une classe public abstract qui étend RoomDatabase. La nouvelle classe abstraite que vous avez définie fait office de conteneur de base de données. Elle est abstraite, car Room crée l'implémentation à votre place.
  • Annotez la classe avec @Database. Dans les arguments, répertoriez les entités de la base de données et définissez le numéro de version.
  • Définissez une méthode ou une propriété abstraite qui renvoie une instance ItemDao. Room générera alors l'implémentation automatiquement.
  • Vous n'avez besoin que d'une seule instance de RoomDatabase pour l'ensemble de l'application. Vous devez donc faire en sorte que RoomDatabase soit un singleton.
  • Avec Room, utilisez Room.databaseBuilder pour créer votre base de données (item_database) uniquement si elle n'existe pas déjà. Sinon, renvoyez la base de données existante.

Créer la base de données

  1. Dans le package data, créez une classe Kotlin : ItemRoomDatabase.kt.
  2. Dans le fichier ItemRoomDatabase.kt, définissez la classe ItemRoomDatabase en tant que classe abstract (abstraite) qui étend RoomDatabase. Annotez la classe avec @Database. L'erreur qui indique que des paramètres sont manquants sera corrigée à l'étape suivante.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
  1. Pour que Room puisse créer la base de données, l'annotation @Database doit comporter plusieurs arguments.
  • Spécifiez Item en tant que seule classe contenant la liste d'entities (entités).
  • Définissez version sur 1. Chaque fois que vous modifiez le schéma de la table de base de données, vous devez incrémenter le numéro de version.
  • Définissez exportSchema sur false pour ne pas conserver les sauvegardes de l'historique des versions de schéma.
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. La base de données doit connaître le DAO. Dans le corps de la classe, déclarez une fonction abstraite qui renvoie ItemDao. Vous pouvez avoir plusieurs DAO.
abstract fun itemDao(): ItemDao
  1. Sous la fonction abstraite, définissez un objet companion (compagnon). L'objet compagnon permet d'accéder aux méthodes de création ou d'obtention de la base de données en utilisant le nom de classe comme qualificatif.
 companion object {}
  1. Dans l'objet companion, déclarez une INSTANCE, une variable privée pouvant être nulle, avant de l'initialiser en lui attribuant la valeur null. La variable INSTANCE conservera une référence à la base de données, si vous en avez créé une. Ainsi, une seule instance de la base de données est ouverte à tout moment. Cela est particulièrement intéressant, car les bases de données ont besoin de beaucoup de ressources, que ce soit pour leur création ou pour leur gestion.

Annotez la variable INSTANCE avec @Volatile. La valeur d'une variable volatile ne sera jamais mise en cache, et toutes les écritures et lectures seront effectuées vers et depuis la mémoire principale. Cela permet de s'assurer que la valeur de la variable INSTANCE est toujours à jour et identique pour tous les threads d'exécution. En d'autres termes, les modifications apportées à INSTANCE par un thread sont immédiatement visibles par tous les autres.

@Volatile
private var INSTANCE: ItemRoomDatabase? = null
  1. En dessous de INSTANCE, tout en restant dans l'objet companion, définissez une méthode getDatabase() avec un paramètre Context, dont l'outil de création de base de données aura besoin. Renvoyez un type ItemRoomDatabase. Une erreur s'affiche, car getDatabase() ne renvoie rien pour le moment.
fun getDatabase(context: Context): ItemRoomDatabase {}
  1. Il est possible que différents threads se retrouvent dans une condition de concurrence et demandent une instance de base de données en même temps, ce qui a pour effet de dupliquer la base de données. En encapsulant le code pour placer la base de données dans un bloc synchronized, vous vous assurez qu'un seul thread d'exécution à la fois peut y accéder. Ainsi, vous avez la certitude que la base de données n'est initialisée qu'une seule fois.

Dans getDatabase(), renvoyez la variable INSTANCE ou, si sa valeur est null, initialisez-la dans un bloc synchronized{}. Pour ce faire, utilisez l'opérateur Elvis (?:). Transmettez this à l'objet compagnon que vous souhaitez verrouiller dans le bloc de fonction. Vous corrigerez l'erreur au cours des étapes suivantes.

return INSTANCE ?: synchronized(this) { }
  1. Dans le bloc synchronisé, créez une variable d'instance val et utilisez l'outil de création de base de données pour l'obtenir. Des erreurs continueront d'apparaître, vous les corrigerez au cours des étapes suivantes.
val instance = Room.databaseBuilder()
  1. À la fin du bloc synchronized, renvoyez instance.
return instance
  1. Dans le bloc synchronized, initialisez la variable instance et utilisez l'outil de création de base de données pour l'obtenir. Transmettez à Room.databaseBuilder() le contexte de l'application, la classe de base de données et un nom pour celle-ci, item_database.
val instance = Room.databaseBuilder(
   context.applicationContext,
   ItemRoomDatabase::class.java,
   "item_database"
)

Android Studio générera une erreur de non-correspondance de type. Pour supprimer cette erreur, vous devrez ajouter une stratégie de migration et build() au cours des étapes suivantes.

  1. Ajoutez la stratégie de migration requise au créateur. Utilisez .fallbackToDestructiveMigration().

Normalement, vous devez accompagner toute stratégie de migration d'un objet de migration, au cas où le schéma viendrait à être modifié. Un objet de migration définit la façon dont vous prenez toutes les lignes de l'ancien schéma pour les rendre compatibles avec le nouveau, de sorte qu'aucune donnée ne soit perdue. Les principes de la migration dépassent le cadre de cet atelier de programmation. Une solution simple consiste à détruire et à recréer la base de données, ce qui entraîne une perte de données.

.fallbackToDestructiveMigration()
  1. Pour créer l'instance de base de données, appelez .build(). Les erreurs affichées par Android Studio devraient être supprimées.
.build()
  1. Dans le bloc synchronized, attribuez INSTANCE = instance.
INSTANCE = instance
  1. À la fin du bloc synchronized, renvoyez instance. Votre code, une fois fini, doit ressembler à ceci :
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {

   abstract fun itemDao(): ItemDao

   companion object {
       @Volatile
       private var INSTANCE: ItemRoomDatabase? = null
       fun getDatabase(context: Context): ItemRoomDatabase {
           return INSTANCE ?: synchronized(this) {
               val instance = Room.databaseBuilder(
                   context.applicationContext,
                   ItemRoomDatabase::class.java,
                   "item_database"
               )
                   .fallbackToDestructiveMigration()
                   .build()
               INSTANCE = instance
               return instance
           }
       }
   }
}
  1. Compilez votre code pour vous assurer qu'il ne comporte pas d'erreur.

Implémenter la classe d'application

Dans cette tâche, vous allez instancier l'instance de base de données dans la classe d'application.

  1. Ouvrez InventoryApplication.kt, créez un val nommé database de type ItemRoomDatabase. Instanciez database en appelant getDatabase() sur ItemRoomDatabase et en transmettant le contexte. Utilisez le délégué lazy pour que l'instance database soit créée par nécessité, lorsque vous avez besoin de la référence ou y accédez pour la première fois (plutôt que lorsque l'application démarre). Cette opération crée la base de données (la base de données physique sur le disque) lors du premier accès.
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase

class InventoryApplication : Application(){
   val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}

Vous utiliserez l'instance database lorsque vous créerez une instance ViewModel, dans les étapes suivantes de l'atelier de programmation.

Vous disposez désormais de tous les éléments nécessaires à l'utilisation de Room. Ce code peut être compilé et exécuté, mais vous n'avez aucun moyen de savoir s'il fonctionne réellement. C'est donc le moment d'ajouter un élément à votre base de données d'inventaire pour la tester. Pour ce faire, vous avez besoin d'un ViewModel afin de communiquer avec la base de données.

8. Ajouter un ViewModel

Jusqu'à présent, vous avez créé une base de données. Les classes de l'interface utilisateur, pour leur part, faisaient partie du code de démarrage. Pour enregistrer les données temporaires de l'application et accéder à la base de données, vous avez besoin d'un ViewModel. Le ViewModel de l'application Inventory interagira avec la base de données à l'aide du DAO et fournira des données à l'UI. Toutes les opérations de base de données devront être exécutées hors du thread UI principal. Pour ce faire, vous allez devoir utiliser des coroutines et viewModelScope.

91298a7c05e4f5e0.png

Créer un ViewModel pour Inventory

  1. Dans le package com.example.inventory, créez un fichier de classe Kotlin, InventoryViewModel.kt.
  2. Étendez la classe InventoryViewModel à partir de ViewModel. Transmettez l'objet ItemDao en tant que paramètre au constructeur par défaut.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
  1. À la fin du fichier InventoryViewModel.kt, en dehors de la classe, ajoutez la classe InventoryViewModelFactory pour instancier InventoryViewModel. Transmettez le même paramètre de constructeur que InventoryViewModel, à savoir l'instance ItemDao. Étendez la classe à partir de ViewModelProvider.Factory. Au cours de l'étape suivante, vous corrigerez l'erreur concernant les méthodes non implémentées.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
  1. Cliquez sur l'ampoule rouge et sélectionnez Implémenter les membres. Vous pouvez également forcer la méthode create() dans la classe ViewModelProvider.Factory, comme suit. Elle accepte tous les types de classe comme argument et renvoie ViewModel.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
  1. Implémentez la méthode create(). Vérifiez si modelClass est identique à la classe InventoryViewModel et renvoyez une instance de celle-ci. Dans le cas contraire, générez une exception.
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
   @Suppress("UNCHECKED_CAST")
   return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")

Remplir le ViewModel

Dans cette tâche, vous allez remplir la classe InventoryViewModel pour ajouter des données d'inventaire à la base de données. Dans l'application Inventory, observez l'entité Item et l'écran Ajouter un élément.

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

85c644aced4198c5.png

Pour ajouter une entité à la base de données, vous devez indiquer le nom, le prix et le stock de cet élément. Plus tard dans l'atelier de programmation, vous utiliserez l'écran Ajouter un élément pour recevoir ces informations de la part de l'utilisateur. Pour la tâche actuelle, cependant, nous allons nous contenter d'utiliser trois chaînes en tant qu'entrées pour ViewModel, les convertir en instance d'entité Item et les enregistrer dans la base de données à l'aide de l'instance ItemDao. Il est temps d'implémenter tout ceci.

  1. Dans la classe InventoryViewModel, ajoutez une fonction private (privée) appelée insertItem(), qui reçoit un objet Item et ajoute des informations à la base de données de manière non bloquante.
private fun insertItem(item: Item) {
}
  1. Pour interagir avec la base de données hors du thread principal, démarrez une coroutine et appelez-y la méthode DAO. Dans la méthode insertItem(), utilisez viewModelScope.launch pour démarrer une coroutine dans ViewModelScope. Dans la fonction de lancement, appelez la fonction de suspension insert() sur itemDao en transmettant item. ViewModelScope est une propriété d'extension de la classe ViewModel qui annule automatiquement ses coroutines enfants lorsque ViewModel est détruite.
private fun insertItem(item: Item) {
   viewModelScope.launch {
       itemDao.insert(item)
   }
}

Importez kotlinx.coroutines.launch, androidx.lifecycle.viewModelScope

com.example.inventory.data.Item, si cette importation ne s'est pas faite automatiquement.

  1. Dans la classe InventoryViewModel, ajoutez une autre fonction privée qui reçoit trois chaînes de caractères et renvoie une instance Item.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. Toujours dans la classe InventoryViewModel, ajoutez une fonction publique appelée addNewItem() qui reçoit trois chaînes de caractères (les informations sur l'élément à ajouter). Transmettez ces chaînes à la fonction getNewItemEntry(), puis attribuez la valeur renvoyée à un val nommé newItem. Appelez insertItem() pour transmettre newItem afin d'ajouter l'entité à la base de données. Elle sera appelée à partir du fragment d'UI pour ajouter à la base de données les informations sur les éléments.
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
   val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
   insertItem(newItem)
}

Notez que vous n'avez pas utilisé viewModelScope.launch pour addNewItem(), mais que vous en avez besoin ci-dessus dans insertItem(), lorsque vous appelez une méthode DAO. En effet, les fonctions de suspension ne peuvent être appelées qu'à partir d'une coroutine ou d'une autre fonction de suspension. Or, la fonction itemDao.insert(item) est justement une fonction de suspension.

Vous avez ajouté toutes les fonctions requises pour ajouter des entités à la base de données. Dans la tâche suivante, vous allez modifier le fragment Ajouter un élément pour utiliser les fonctions ci-dessus.

9. Modifier AddItemFragment

  1. Dans AddItemFragment.kt, au début de la classe AddItemFragment, créez un élément private val appelé viewModel, de type InventoryViewModel. Utilisez le délégué de propriété Kotlin by activityViewModels() pour appliquer votre ViewModel sur plusieurs fragments. Vous corrigerez l'erreur à l'étape suivante.
private val viewModel: InventoryViewModel by activityViewModels {
}
  1. Dans le lambda, appelez le constructeur InventoryViewModelFactory() et transmettez l'instance ItemDao. Utilisez l'instance database que vous avez créée dans l'une des tâches précédentes pour appeler le constructeur itemDao.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
  1. Sous la définition viewModel, créez un lateinit var nommé item, de type Item.
 lateinit var item: Item
  1. L'écran Ajouter un élément contient trois champs de texte permettant à l'utilisateur de transmettre les informations relatives au nouvel élément. Au cours de cette étape, vous allez ajouter une fonction pour vérifier que les champs de texte ne sont pas vides. Vous utiliserez cette fonction pour vérifier les entrées utilisateur avant d'ajouter ou de modifier l'entité dans la base de données. Cette validation doit être effectuée dans ViewModel et non dans le fragment. Dans la classe InventoryViewModel, ajoutez la fonction public (publique) suivante, appelée isEntryValid().
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
   if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
       return false
   }
   return true
}
  1. Dans AddItemFragment.kt, sous la fonction onCreateView(), créez une fonction private appelée isEntryValid(), qui renvoie un Boolean (valeur booléenne). L'erreur qui indique que la valeur renvoyée est manquante sera corrigée à l'étape suivante.
private fun isEntryValid(): Boolean {
}
  1. Dans la classe AddItemFragment, implémentez la fonction isEntryValid(). Appelez la fonction isEntryValid() sur l'instance viewModel en transmettant le texte issu des champs. Renvoyez la valeur de la fonction viewModel.isEntryValid().
private fun isEntryValid(): Boolean {
   return viewModel.isEntryValid(
       binding.itemName.text.toString(),
       binding.itemPrice.text.toString(),
       binding.itemCount.text.toString()
   )
}
  1. Dans la classe AddItemFragment, sous la fonction isEntryValid(), ajoutez une autre fonction private (privée) appelée addNewItem(). Elle ne comporte pas de paramètre et ne renvoie rien. Dans cette fonction, appelez isEntryValid() dans la condition if.
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
  1. Dans le bloc if, appelez la méthode addNewItem() sur l'instance viewModel. Transmettez les informations sur l'élément saisies par l'utilisateur et utilisez l'instance binding pour les lire.
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
  1. Sous le bloc if, créez une action val pour revenir à ItemListFragment. Appelez findNavController().navigate() en transmettant votre action.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

Importez androidx.navigation.fragment.findNavController.

  1. La méthode terminée doit se présenter comme suit.
private fun addNewItem() {
       if (isEntryValid()) {
           viewModel.addNewItem(
               binding.itemName.text.toString(),
               binding.itemPrice.text.toString(),
               binding.itemCount.text.toString(),
           )
           val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
           findNavController().navigate(action)
       }
}
  1. Pour unifier le tout, ajoutez un gestionnaire de clics au bouton Save (Enregistrer). Dans la classe AddItemFragment, au-dessus de la fonction onDestroyView(), forcez onViewCreated().
  2. Dans la fonction onViewCreated(), ajoutez un gestionnaire de clics au bouton d'enregistrement et appelez addNewItem() à partir de celui-ci.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
  1. Compilez et exécutez votre application. Appuyez sur le bouton d'action flottant +. Sur l'écran Ajouter un élément, ajoutez les détails du nouvel élément et appuyez sur Enregistrer. Cette action enregistre les données, mais pour l'instant, vous ne voyez rien dans l'application. Dans la tâche suivante, vous allez utiliser l'outil d'inspection de bases de données pour afficher les données que vous avez enregistrées.

193c7fa9c41e0819.png

Afficher la base de données à l'aide de l'outil d'inspection

  1. Exécutez votre application sur un émulateur ou un appareil connecté avec un niveau d'API 26 ou supérieur, si vous ne l'avez pas déjà fait. L'outil d'inspection de bases de données fonctionne mieux sur les émulateurs ou appareils utilisant ce niveau d'API.
  2. Dans la barre de menu d'Android Studio, sélectionnez Affichage > Fenêtres d'outil > Outil d'inspection de bases de données.
  3. Dans le menu déroulant du volet Outil d'inspection de bases de données, sélectionnez com.example.inventory.
  4. Le fichier item_database de l'application Inventory apparaît dans le volet Bases de données. Développez le nœud du fichier item_database et sélectionnez Élément pour l'inspecter. Si le volet Bases de données est vide, utilisez votre émulateur pour ajouter des éléments à la base de données à l'aide de l'écran Ajouter un élément.
  5. Cochez la case Live updates (Mises à jour en direct) dans l'outil d'inspection pour mettre à jour automatiquement les données qu'il présente lorsque vous interagissez avec votre application en cours d'exécution dans l'émulateur ou sur l'appareil.

4803c08f94e34118.png

Félicitations ! Vous avez créé une application avec des données persistantes à l'aide de Room. Dans le prochain atelier de programmation, vous ajouterez un RecyclerView à votre application pour afficher les éléments de la base de données. Vous ajouterez également de nouvelles fonctionnalités à l'application, comme la suppression et la modification d'entités. À bientôt !

10. 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é (qui 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é

  • Définissez vos tables en tant que classes de données annotées avec @Entity. Définissez les propriétés annotées avec @ColumnInfo en tant que colonnes dans les tables.
  • Définissez un objet d'accès aux données (DAO) en tant qu'interface annotée avec @Dao. Le DAO mappe les fonctions Kotlin aux requêtes de base de données.
  • Utilisez les annotations pour définir les fonctions @Insert, @Delete et @Update.
  • Pour toutes les autres requêtes, utilisez l'annotation @Query avec une chaîne de requête SQLite comme paramètre.
  • Utilisez l'outil d'inspection de bases de données pour afficher les données enregistrées dans la base de données Android SQLite.

12. En savoir plus

Documentation pour les développeurs Android

Articles de blog

Vidéos

Autres articles et documentation supplémentaire