Persistance des données avec Room

1. Avant de commencer

La plupart des applications de qualité en production reposent sur la persistance des données. Elles peuvent, par exemple, 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 de tels cas d'utilisation, vous avez besoin d'une base de données pour stocker ces données persistantes.

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 de base de données. Au lieu de se servir directement de SQLite, Room simplifie les tâches de configuration de la base de données, ainsi que les interactions avec l'application. Room permet également de vérifier les instructions SQLite lors de la compilation.

Une couche d'abstraction est un ensemble de fonctions qui masquent l'implémentation/la complexité sous-jacente. Elle sert d'interface à un ensemble de fonctionnalités existant, comme SQLite dans ce cas.

L'image ci-dessous montre comment Room s'intègre, en tant que source de données, à l'architecture globale recommandée dans ce cours. Room est une source de données.

La couche de données contient des dépôts et des sources de données

Conditions préalables

  • Vous savez comment créer une UI (interface utilisateur) de base pour une application Android à l'aide de Jetpack Compose.
  • Vous savez utiliser des composables comme Text, Icon, IconButton et LazyColumn.
  • Vous savez utiliser le composable NavHost pour définir des routes et des écrans dans votre application.
  • Vous savez naviguer entre les écrans à l'aide d'un NavHostController.
  • Vous connaissez le composant d'architecture Android ViewModel. Vous savez utiliser ViewModelProvider.Factory pour instancier les ViewModels.
  • Vous connaissez les principes fondamentaux de la simultanéité.
  • Vous savez utiliser des coroutines pour les tâches de longue durée.
  • Vous connaissez les principes de fonctionnement des bases de données SQLite et du langage SQL.

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 objet d'accès aux données (DAO, Data Access Object) et de classes de base de données
  • Utilisation d'un DAO 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 avec Android Studio
  • Un appareil ou un émulateur avec le niveau d'API 26 ou supérieur

2. Présentation de l'application

Dans cet atelier de programmation, vous utiliserez le code de démarrage de l'application Inventory et y ajouterez le calque de base de données à l'aide de la bibliothèque Room. La version finale de l'application affichera la liste des articles issus de la base de données d'inventaire. L'utilisateur pourra ajouter, supprimer ou modifier des articles de la base de données d'inventaire. Pour cet atelier de programmation, vous enregistrerez les données des articles dans la base de données Room. Vous réaliserez les autres fonctionnalités de l'application dans le prochain atelier de programmation.

Écran de téléphone avec des articles d'inventaire

Écran "Add Item" (Ajouter un article) affiché sur l'écran du téléphone.

Écran "Add Item" (Ajouter un article) avec les détails de l'article renseignés.

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

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

Pour commencer, téléchargez le code de démarrage :

Vous pouvez également cloner le dépôt GitHub du code :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout starter

Vous pouvez parcourir le code dans le dépôt GitHub Inventory app.

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. Assurez-vous que l'émulateur ou l'appareil connecté fonctionne avec 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 au minimum ce niveau d'API.
  1. Notez que l'application n'affiche aucune donnée d'inventaire.
  2. Appuyez sur le bouton d'action flottant pour ajouter des articles à la base de données.

L'application ouvre un nouvel écran dans lequel vous pouvez saisir les détails du nouvel élément.

Sur un téléphone, l'application affiche un inventaire vide

Écran "Add Item" (Ajouter un article) affiché sur l'écran du téléphone.

Problèmes avec le code de démarrage

  1. Sur l'écran Add Item (Ajouter un article), saisissez les détails de l'article, tels que son nom, son prix et sa quantité.
  2. Appuyez sur Save (Enregistrer). L'écran Add Item (Ajouter un article) ne se ferme pas, mais vous pouvez revenir en arrière à l'aide de la touche Retour. Comme la fonctionnalité d'enregistrement n'est pas encore implémentée, les détails de l'article ne seront pas enregistrés.

Notez que l'application est incomplète et que le bouton Save (Enregistrer) n'est pas implémenté.

Écran "Add Item" (Ajouter un article) avec les détails de l'article renseignés.

Dans cet atelier de programmation, vous devez ajouter le code qui utilisera Room pour enregistrer les détails de l'inventaire dans la base de données SQLite. Vous vous servirez de la bibliothèque de persistance de Room pour interagir avec la base de données SQLite.

Tutoriel du code

Le code de démarrage que vous avez téléchargé présente des mises en page d'écran prédéfinies. Dans ce parcours, vous allez vous concentrer sur l'implémentation de la logique de base de données. La section suivante présente brièvement quelques fichiers pour vous aider à démarrer.

ui/home/HomeScreen.kt

Ce fichier correspond à l'écran d'accueil, qui est le premier écran de l'application. Il contient les composables permettant d'afficher la liste d'inventaire. Il dispose d'un bouton d'action flottant + permettant d'ajouter des articles à cette liste. Vous afficherez les articles de la liste ultérieurement dans ce parcours.

Écran de téléphone avec des articles d'inventaire

ui/item/ItemEntryScreen.kt

Cet écran est semblable à ItemEditScreen.kt. Ils comportent tous deux des champs de texte pour les détails de l'article. Cet écran s'affiche lorsque l'utilisateur appuie sur le bouton d'action flottant sur l'écran d'accueil. ItemEntryViewModel.kt est le ViewModel correspondant à cet écran.

Écran "Add Item" (Ajouter un article) avec les détails de l'article renseignés.

ui/navigation/InventoryNavGraph.kt

Ce fichier est le graphique de navigation pour toute l'application.

4. Principaux composants de Room

Kotlin permet de gérer facilement les données via des classes. Bien que les classes facilitent la gestion des données en mémoire, pour la persistance des données, vous devez convertir ces données dans un format compatible avec le stockage de base de données. Pour ce faire, vous avez besoin de tables afin de pouvoir stocker les données, et de requêtes pour pouvoir accéder aux données et les modifier.

Les trois composants de Room que nous allons présenter ici simplifient ces workflows.

  • Les entités Room représentent les tables de la base de données de votre application. Vous pouvez vous servir de ces entités pour mettre à jour les données stockées en lignes dans ces tables, mais aussi pour créer des lignes afin d'insérer d'autres données.
  • Les DAO fournissent des méthodes grâce auxquelles votre application peut récupérer, mettre à jour, insérer et supprimer des données dans la base de données.
  • La classe Database Room est la classe de base de données qui fournit à votre application les instances des DAO associées à 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 interagissent pour communiquer avec la base de données.

a3288e8f37250031.png

Ajouter des dépendances Room

Dans cette tâche, vous allez ajouter les bibliothèques de composants Room requises dans vos fichiers Gradle.

  1. Ouvrez le fichier Gradle build.gradle.kts (Module: InventoryApp.app) au niveau du module.
  2. Dans le bloc dependencies, ajoutez les dépendances de la bibliothèque Room indiquées dans le code suivant.
//Room
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")

KSP est une API à la fois puissante et simple permettant d'analyser les annotations Kotlin.

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é contient des informations sur les articles de l'inventaire, telles que leur nom, leur prix et leur quantité.

8c9f1659ee82ca43.png

L'annotation @Entity caractérise une classe d'entité de base de données. Pour chaque classe d'entité, l'application crée une table de base de données afin de stocker les articles. 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 correspondante. Chaque instance d'entité stockée dans la base de données doit posséder 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 que l'application a attribué une clé primaire, elle ne peut plus être modifiée. Elle représente l'objet de l'entité tant qu'elle existe dans la base de données.

Dans cette tâche, vous allez créer une classe d'entité et définir des champs permettant de stocker les informations d'inventaire suivantes pour chaque article : Int pour stocker la clé primaire, String pour stocker le nom de l'article, double pour stocker le prix de l'article et Int pour stocker la quantité en stock.

  1. Ouvrez le code de démarrage dans Android Studio.
  2. Ouvrez le package data sous le package de base com.example.inventory.
  3. Dans le package data, ouvrez la classe Kotlin Item, qui représente une entité de base de données dans votre application.
// No need to copy over, this is part of the starter code
class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)

Classes de données

Les classes de données sont principalement utilisées pour stocker des données dans Kotlin. Elles sont définies avec le mot clé data. Les objets de classe de données Kotlin présentent des avantages supplémentaires. Par exemple, 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 firstName: String, val lastName: 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 principaux du constructeur doivent être val ou var.
  • Les classes de données ne peuvent pas être abstract, open ni sealed.

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

  1. Ajoutez le mot clé data comme préfixe à la définition de la classe Item afin de la convertir en classe de données.
data class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: 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 items comme nom de table SQLite.
import androidx.room.Entity

@Entity(tableName = "items")
data class Item(
   ...
)
  1. Annotez la propriété id avec @PrimaryKey pour faire d'id la clé primaire. Une clé primaire est une valeur permettant d'identifier de manière unique chaque enregistrement/entrée de votre table Item.
import androidx.room.PrimaryKey

@Entity(tableName = "items")
data class Item(
    @PrimaryKey
    val id: Int,
    ...
)
  1. Attribuez à id la valeur par défaut 0, nécessaire pour que id puisse générer automatiquement des valeurs id.
  2. Ajoutez le paramètre autoGenerate à l'annotation @PrimaryKey pour spécifier si la colonne de clé primaire doit être générée automatiquement. Si autoGenerate est défini sur true, Room générera automatiquement une valeur unique pour la colonne de clé primaire lorsqu'une nouvelle instance d'entité est insérée dans la base de données. Ainsi, chaque instance d'entité dispose d'un identifiant unique, sans avoir à attribuer manuellement des valeurs à la colonne de clé primaire.
data class Item(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    // ...
)

Félicitations ! Maintenant que vous avez créé une classe d'entité, vous pouvez créer un objet d'accès aux données (DAO) pour accéder à la base de données.

6. Créer le DAO

L'objet d'accès aux données (DAO, Data Access Object) est un modèle que vous pouvez utiliser pour séparer la couche de persistance du reste de l'application en fournissant une interface abstraite. Cette séparation respecte le principe de responsabilité unique, que nous avons abordé 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 vous permet de modifier la couche de données indépendamment du code qui les utilise.

8b91b8bbd7256a63.png

Dans cette tâche, vous allez définir un 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 est une interface personnalisée qui propose des moyens pratiques d'interroger, de récupérer, d'insérer, de supprimer et de mettre à jour la base de données. Room générera une implémentation de cette classe au moment de la compilation.

La bibliothèque Room fournit des annotations pratiques, comme @Insert, @Delete et @Update, pour définir des méthodes qui effectuent des insertions, des suppressions et des mises à jour simples sans que vous ayez besoin d'écrire une instruction SQL.

Si vous devez définir des insertions, des suppressions ou des mises à jour plus complexes, ou si vous devez interroger les données dans la base de données, utilisez plutôt une annotation @Query.

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 d'inventaire, vous devez être en mesure d'effectuer les opérations suivantes :

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

59aaa051e6a22e79.png

Pour implémenter le DAO dans votre application, procédez comme suit :

  1. Dans le package data, créez l'interface Kotlin ItemDao.kt.

Le champ "Name" (Nom) indique "ItemDao".

  1. Annotez l'interface ItemDao avec @Dao.
import androidx.room.Dao

@Dao
interface ItemDao {
}
  1. Dans le corps de l'interface, ajoutez une annotation @Insert.
  2. Sous @Insert, ajoutez une fonction insert() qui reçoit une instance de la classe Entity appelée item comme argument.
  3. Marquez la fonction avec le mot clé suspend pour qu'elle s'exécute sur un thread distinct.

L'exécution des opérations de base de données peut prendre beaucoup de temps. Elles doivent donc s'effectuer sur un thread distinct. Room ne permet pas d'accéder à la base de données sur le thread principal.

import androidx.room.Insert

@Insert
suspend fun insert(item: Item)

Lorsque des articles sont insérés dans la base de données, des conflits peuvent se produire. Par exemple, le code peut essayer à plusieurs endroits différents de mettre à jour l'entité avec des valeurs différentes et incompatibles, comme la même clé primaire. Une entité correspond à une ligne dans la base de données. Dans l'appli Inventory, nous n'insérons l'entité que depuis l'écran Add Item (Ajouter un article). Aucun conflit ne devrait donc se produire, et nous pouvons définir cette option sur Ignore (Ignorer).

  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 un nouvel article.

Pour en savoir plus sur les stratégies de gestion de conflit disponibles, consultez la documentation OnConflictStrategy.

import androidx.room.OnConflictStrategy

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

Room va maintenant générer tout le code nécessaire pour insérer item dans la base de données. Lorsque vous appelez l'une des fonctions DAO marquées d'annotations Room, Room exécute la requête SQL correspondante sur la base de données. Par exemple, lorsque vous appelez la méthode ci-dessus (insert()) à partir du code Kotlin, Room exécute une requête SQL pour insérer l'entité dans la base de données.

  1. Ajoutez une fonction avec une annotation @Update qui utilise un paramètre Item.

L'entité mise à jour a la même clé primaire que l'entité transmise. Vous pouvez mettre à jour tout ou partie des autres propriétés de l'entité.

  1. Comme pour la méthode insert(), marquez cette fonction avec le mot clé suspend.
import androidx.room.Update

@Update
suspend fun update(item: Item)

Ajoutez une autre fonction comportant l'annotation @Delete pour supprimer des articles et en faire une fonction de suspension.

import androidx.room.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 article spécifique de la table en fonction de l'id donné. Le code suivant fournit un exemple de requête qui sélectionne toutes les colonnes depuis items, où id correspond à une valeur spécifique et id est un identifiant unique.

Exemple :

// Example, no need to copy over
SELECT * from items WHERE id = 1
  1. Ajoutez une annotation @Query.
  2. Utilisez la requête SQLite de l'étape précédente en tant que paramètre de chaîne dans l'annotation @Query.
  3. Ajoutez un paramètre String à @Query. Il s'agit d'une requête SQLite permettant de récupérer un article dans la table.

La requête indique désormais de sélectionner toutes les colonnes depuis items, où id correspond à l'argument :id. Notez que :id utilise le caractère deux-points, ce qui permet à la requête de faire référence à des arguments dans la fonction.

@Query("SELECT * from items WHERE id = :id")
  1. Après l'annotation @Query, ajoutez une fonction getItem() qui accepte un argument Int et renvoie un Flow<Item>.
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>

Il est recommandé d'utiliser Flow dans la couche de persistance. Avec Flow comme type renvoyé, vous recevez une notification chaque fois que les données de la base de données changent. Room veille automatiquement à ce que Flow reste à jour. Vous n'avez donc besoin de générer les données de façon explicite qu'une seule fois. Cette configuration est 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.

  1. Ajoutez une annotation @Query avec une fonction getAllItems().
  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 à getAllItems() de renvoyer la liste des entités Item en tant que Flow. Room veille automatiquement à ce que Flow reste à jour. Vous n'avez donc besoin de générer les données de façon explicite qu'une seule fois.
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>

ItemDao terminé :

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface ItemDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(item: Item)

    @Update
    suspend fun update(item: Item)

    @Delete
    suspend fun delete(item: Item)

    @Query("SELECT * from items WHERE id = :id")
    fun getItem(id: Int): Flow<Item>

    @Query("SELECT * from items ORDER BY name ASC")
    fun getAllItems(): Flow<List<Item>>
}
  1. Bien qu'aucune modification ne soit visible, compilez 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 un objet RoomDatabase qui utilisera l'élément Entity et le DAO des tâches précédentes. La classe de base de données définit la liste des entités et des DAO.

La classe Database fournit à votre application les instances des DAO que vous définissez. 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. Elle peut également se servir des 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 et l'annoter avec @Database. Cette classe comporte une méthode qui renvoie l'instance existante de RoomDatabase si la base de données n'existe pas.

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, listez 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 InventoryDatabase.kt.
  2. Dans le fichier InventoryDatabase.kt, définissez la classe InventoryDatabase en tant que classe abstract (abstraite) qui étend RoomDatabase.
  3. Annotez la classe avec @Database. Ignorez l'erreur de paramètre manquant, que vous corrigerez à l'étape suivante.
import androidx.room.Database
import androidx.room.RoomDatabase

@Database
abstract class InventoryDatabase : RoomDatabase() {}

Pour que Room puisse créer la base de données, l'annotation @Database doit comporter plusieurs arguments.

  1. Spécifiez Item en tant que seule classe contenant la liste d'entities (entités).
  2. Définissez version sur 1. Chaque fois que vous modifiez le schéma de la table de base de données, vous devez augmenter le numéro de version.
  3. 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. Dans le corps de la classe, déclarez une fonction abstraite qui renvoie ItemDao afin que la base de données puisse identifier le DAO.
abstract fun itemDao(): ItemDao
  1. Sous la fonction abstraite, définissez un companion object, qui permettra d'accéder aux méthodes pour créer ou générer la base de données et qui utilisera le nom de la 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 à la fois. 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.

  1. Annotez la variable Instance avec @Volatile.

La valeur d'une variable volatile n'est jamais mise en cache, et toutes les lectures et écritures proviennent de la mémoire principale. Ces fonctionnalités permettent de s'assurer que la valeur d'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 s'affichent immédiatement pour tous les autres.

@Volatile
private var Instance: InventoryDatabase? = null
  1. Sous 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.
  2. Renvoyez un type InventoryDatabase. Un message d'erreur s'affiche, car getDatabase() ne renvoie aucun élément pour le moment.
import android.content.Context

fun getDatabase(context: Context): InventoryDatabase {}

Plusieurs threads peuvent demander une instance de base de données en même temps, ce qui génère deux bases de données au lieu d'une seule. C'est ce que l'on appelle une condition de concurrence. 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. Utilisez le bloc synchronized{} pour éviter la condition de concurrence.

  1. Dans getDatabase(), renvoyez la variable Instance ou, si sa valeur Instance est null, initialisez-la dans un bloc synchronized{}. Pour ce faire, utilisez l'opérateur Elvis (?:).
  2. Transmettez this, l'objet associé. Vous corrigerez l'erreur plus tard.
return Instance ?: synchronized(this) { }
  1. Dans le bloc synchronisé, utilisez l'outil de création de base de données pour obtenir la base de données. Continuez à ignorer les erreurs. Vous les corrigerez ultérieurement.
import androidx.room.Room

Room.databaseBuilder()
  1. Dans le bloc synchronized, utilisez l'outil de création de base de données pour obtenir une base de données. Transmettez à Room.databaseBuilder() le contexte de l'application, la classe de base de données et un nom pour celle-ci, item_database.
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")

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

  1. Ajoutez la stratégie de migration requise au compilateur. Utilisez . fallbackToDestructiveMigration().
.fallbackToDestructiveMigration()
  1. Pour créer l'instance de base de données, appelez .build(). Cette action supprime les erreurs Android Studio.
.build()
  1. Après build(), ajoutez un bloc also et attribuez Instance = it pour conserver une référence à l'instance de base de données qui vient d'être créée.
.also { Instance = it }
  1. À la fin du bloc synchronized, renvoyez instance. Le code final se présente comme suit :
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

/**
* Database class with a singleton Instance object.
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class InventoryDatabase : RoomDatabase() {

    abstract fun itemDao(): ItemDao

    companion object {
        @Volatile
        private var Instance: InventoryDatabase? = null

        fun getDatabase(context: Context): InventoryDatabase {
            // if the Instance is not null, return it, otherwise create a new database instance.
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
                    .build()
                    .also { Instance = it }
            }
        }
    }
}
  1. Compilez le code pour vous assurer qu'il ne comporte pas d'erreur.

8. Implémenter le dépôt

Dans cette tâche, vous allez implémenter l'interface ItemsRepository et la classe OfflineItemsRepository pour fournir les entités get, insert, delete et update à partir de la base de données.

  1. Ouvrez le fichier ItemsRepository.kt sous le package data.
  2. Ajoutez les fonctions suivantes à l'interface, qui correspondent à l'implémentation des DAO.
import kotlinx.coroutines.flow.Flow

/**
* Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
*/
interface ItemsRepository {
    /**
     * Retrieve all the items from the the given data source.
     */
    fun getAllItemsStream(): Flow<List<Item>>

    /**
     * Retrieve an item from the given data source that matches with the [id].
     */
    fun getItemStream(id: Int): Flow<Item?>

    /**
     * Insert item in the data source
     */
    suspend fun insertItem(item: Item)

    /**
     * Delete item from the data source
     */
    suspend fun deleteItem(item: Item)

    /**
     * Update item in the data source
     */
    suspend fun updateItem(item: Item)
}
  1. Ouvrez le fichier OfflineItemsRepository.kt sous le package data.
  2. Transmettez un paramètre constructeur de type ItemDao.
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
  1. Dans la classe OfflineItemsRepository, remplacez les fonctions définies dans l'interface ItemsRepository et appelez les fonctions correspondantes à partir d'ItemDao.
import kotlinx.coroutines.flow.Flow

class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
    override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()

    override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)

    override suspend fun insertItem(item: Item) = itemDao.insert(item)

    override suspend fun deleteItem(item: Item) = itemDao.delete(item)

    override suspend fun updateItem(item: Item) = itemDao.update(item)
}

Implémenter la classe AppContainer

Dans cette tâche, vous allez instancier la base de données et transmettre l'instance de DAO à la classe OfflineItemsRepository.

  1. Ouvrez le fichier AppContainer.kt sous le package data.
  2. Transmettez l'instance ItemDao() au constructeur OfflineItemsRepository.
  3. Instanciez l'instance de base de données. Pour ce faire, appelez getDatabase() au niveau de la classe InventoryDatabase en transmettant le contexte, puis appelez .itemDao() pour créer l'instance de Dao.
override val itemsRepository: ItemsRepository by lazy {
    OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao())
}

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 de tester votre base de données. Pour effectuer le test, le ViewModel doit communiquer avec la base de données.

9. Ajouter la fonctionnalité d'enregistrement

Jusqu'à présent, vous avez créé une base de données. Les classes de l'UI, 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 devez mettre à jour les ViewModel. Vos ViewModels interagissent avec la base de données via le DAO et fournissent des données à l'UI. Toutes les opérations de base de données doivent être exécutées à partir du thread UI principal. Pour ce faire, vous devrez utiliser des coroutines et viewModelScope.

Tutoriel de la classe d'état de l'UI

Ouvrez le fichier ui/item/ItemEntryViewModel.kt. La classe de données ItemUiState représente l'état de l'UI d'un élément. La classe de données ItemDetails représente un seul élément.

Le code de démarrage fournit trois fonctions d'extension :

  • La fonction d'extension ItemDetails.toItem() convertit l'objet d'état de l'UI ItemUiState en type d'entité Item.
  • La fonction d'extension Item.toItemUiState() convertit l'objet d'entité Room Item en type d'état d'UI ItemUiState.
  • La fonction d'extension Item.toItemDetails() convertit l'objet d'entité Room Item en ItemDetails.
// No need to copy, this is part of starter code
/**
* Represents Ui State for an Item.
*/
data class ItemUiState(
    val itemDetails: ItemDetails = ItemDetails(),
    val isEntryValid: Boolean = false
)

data class ItemDetails(
    val id: Int = 0,
    val name: String = "",
    val price: String = "",
    val quantity: String = "",
)

/**
* Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is
* not a valid [Double], then the price will be set to 0.0. Similarly if the value of
* [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0
*/
fun ItemDetails.toItem(): Item = Item(
    id = id,
    name = name,
    price = price.toDoubleOrNull() ?: 0.0,
    quantity = quantity.toIntOrNull() ?: 0
)

fun Item.formatedPrice(): String {
    return NumberFormat.getCurrencyInstance().format(price)
}

/**
* Extension function to convert [Item] to [ItemUiState]
*/
fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
    itemDetails = this.toItemDetails(),
    isEntryValid = isEntryValid
)

/**
* Extension function to convert [Item] to [ItemDetails]
*/
fun Item.toItemDetails(): ItemDetails = ItemDetails(
    id = id,
    name = name,
    price = price.toString(),
    quantity = quantity.toString()
)

Vous utiliserez la classe ci-dessus dans les modèles de vue pour lire et mettre à jour l'UI.

Mettre à jour l'ItemEntryViewModel

Dans cette tâche, vous allez transmettre le dépôt au fichier ItemEntryViewModel.kt. Vous pouvez également enregistrer dans la base de données les détails de l'article saisis sur l'écran Ajouter Item (Ajouter un article).

  1. Notez la fonction privée validateInput() dans la classe ItemEntryViewModel.
// No need to copy over, this is part of starter code
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
    return with(uiState) {
        name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
    }
}

La fonction ci-dessus vérifie si les éléments name, price et quantity sont 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.

  1. Ouvrez la classe ItemEntryViewModel et ajoutez un paramètre constructeur par défaut private de type ItemsRepository.
import com.example.inventory.data.ItemsRepository

class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
  1. Mettez à jour initializer pour le modèle de vue d'entrée d'article dans ui/AppViewModelProvider.kt et transmettez l'instance de dépôt en tant que paramètre.
object AppViewModelProvider {
    val Factory = viewModelFactory {
        // Other Initializers
        // Initializer for ItemEntryViewModel
        initializer {
            ItemEntryViewModel(inventoryApplication().container.itemsRepository)
        }
        //...
    }
}
  1. Accédez au fichier ItemEntryViewModel.kt, et à la fin de la classe ItemEntryViewModel, ajoutez une fonction de suspension appelée saveItem() pour insérer un article dans la base de données Room. Cette fonction ajoutera les données à la base de données de manière non bloquante.
suspend fun saveItem() {
}
  1. Dans la fonction, vérifiez si itemUiState est valide et convertissez-le en type Item pour que Room puisse comprendre les données.
  2. Appelez insertItem() sur itemsRepository et transmettez les données. L'UI appellera cette fonction pour ajouter des détails sur les articles à la base de données.
suspend fun saveItem() {
    if (validateInput()) {
        itemsRepository.insertItem(itemUiState.itemDetails.toItem())
    }
}

Vous avez à présent ajouté toutes les fonctions requises pour ajouter des entités à la base de données. Dans la tâche suivante, vous allez mettre à jour l'UI pour qu'elle utilise les fonctions ci-dessus.

Tutoriel du composable ItemEntryBody()

  1. Dans le fichier ui/item/ItemEntryScreen.kt, le composable ItemEntryBody() est partiellement implémenté pour vous dans le code de démarrage. Examinez le composable ItemEntryBody() dans l'appel de fonction ItemEntryScreen().
// No need to copy over, part of the starter code
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = { },
    modifier = Modifier
        .padding(innerPadding)
        .verticalScroll(rememberScrollState())
        .fillMaxWidth()
)
  1. Notez que l'état de l'UI et le lambda updateUiState sont transmis en tant que paramètres de fonction. Consultez la définition de la fonction pour déterminer comment l'état de l'UI est mis à jour.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
    itemUiState: ItemUiState,
    onItemValueChange: (ItemUiState) -> Unit,
    onSaveClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        // ...
    ) {
        ItemInputForm(
             itemDetails = itemUiState.itemDetails,
             onValueChange = onItemValueChange,
             modifier = Modifier.fillMaxWidth()
         )
        Button(
             onClick = onSaveClick,
             enabled = itemUiState.isEntryValid,
             shape = MaterialTheme.shapes.small,
             modifier = Modifier.fillMaxWidth()
         ) {
             Text(text = stringResource(R.string.save_action))
         }
    }
}

Vous afficherez ItemInputForm et un bouton Save (Enregistrer) dans ce composable. Dans le composable ItemInputForm(), vous afficherez trois champs de texte. L'option Save (Enregistrer) n'est activée que si vous saisissez du texte dans les champs de texte. La valeur isEntryValid s'applique si le texte dans tous les champs de texte est valide (autrement dit, si les champs ne sont pas vides).

Écran de téléphone avec les détails de l'article partiellement remplis et le bouton Save (Enregistrer) désactivé

Écran de téléphone avec les détails de l'article remplis et le bouton Save (Enregistrer) activé

  1. Examinez l'implémentation de la fonction composable ItemInputForm() et notez le paramètre de la fonction onValueChange. La valeur itemDetails sera remplacée par la valeur saisie par l'utilisateur dans les champs de texte. Lorsque le bouton Save (Enregistrer) sera activé, itemUiState.itemDetails aura les valeurs à enregistrer.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
    //...
) {
    Column(
        // ...
    ) {
        ItemInputForm(
             itemDetails = itemUiState.itemDetails,
             //...
         )
        //...
    }
}
// No need to copy over, part of the starter code
@Composable
fun ItemInputForm(
    itemDetails: ItemDetails,
    modifier: Modifier = Modifier,
    onValueChange: (ItemUiState) -> Unit = {},
    enabled: Boolean = true
) {
    Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        OutlinedTextField(
            value = itemUiState.name,
            onValueChange = { onValueChange(itemDetails.copy(name = it)) },
            //...
        )
        OutlinedTextField(
            value = itemUiState.price,
            onValueChange = { onValueChange(itemDetails.copy(price = it)) },
            //...
        )
        OutlinedTextField(
            value = itemUiState.quantity,
            onValueChange = { onValueChange(itemDetails.copy(quantity = it)) },
            //...
        )
    }
}

Ajouter un écouteur de clics au bouton "Save" (Enregistrer)

Pour unifier le tout, ajoutez un gestionnaire de clics au bouton Save (Enregistrer). Dans le gestionnaire de clics, lancez une coroutine et appelez saveItem() pour enregistrer les données dans la base de données Room.

  1. Dans ItemEntryScreen.kt, dans la fonction composable ItemEntryScreen, créez un attribut val nommé coroutineScope avec la fonction composable rememberCoroutineScope().
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Mettez à jour l'appel de la fonction ItemEntryBody() et lancez une coroutine dans le lambda onSaveClick.
ItemEntryBody(
   // ...
    onSaveClick = {
        coroutineScope.launch {
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. Examinez l'implémentation de la fonction saveItem() dans le fichier ItemEntryViewModel.kt pour vérifier si itemUiState est valide, en convertissant itemUiState en type Item et en l'insérant dans la base de données à l'aide d'itemsRepository.insertItem().
// No need to copy over, you have already implemented this as part of the Room implementation

suspend fun saveItem() {
    if (validateInput()) {
        itemsRepository.insertItem(itemUiState.itemDetails.toItem())
    }
}
  1. Dans ItemEntryScreen.kt, dans la fonction composable ItemEntryScreen, dans la coroutine, appelez viewModel.saveItem() pour enregistrer l'élément dans la base de données.
ItemEntryBody(
    // ...
    onSaveClick = {
        coroutineScope.launch {
            viewModel.saveItem()
        }
    },
    //...
)

Notez que vous n'avez pas utilisé viewModelScope.launch() pour saveItem() dans le fichier ItemEntryViewModel.kt, mais que vous en avez besoin pour ItemEntryBody() lorsque vous appelez une méthode de dépôt. Vous ne pouvez appeler des fonctions de suspension qu'à partir d'une coroutine ou d'une autre fonction de suspension. La fonction viewModel.saveItem() est une fonction de suspension.

  1. Compilez et exécutez votre application.
  2. Appuyez sur le bouton d'action flottant +.
  3. Sur l'écran Add Item (Ajouter un article), ajoutez les détails du nouvel article et appuyez sur Save (Enregistrer). Notez que le bouton Save (Enregistrer) ne ferme pas l'écran Add Item (Ajouter un article).

Écran de téléphone avec les détails de l'article remplis et le bouton Save (Enregistrer) activé

  1. Dans le lambda onSaveClick, ajoutez un appel à navigateBack() après l'appel à viewModel.saveItem() pour revenir à l'écran précédent. Votre fonction ItemEntryBody() devrait ressembler au code suivant :
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = {
        coroutineScope.launch {
            viewModel.saveItem()
            navigateBack()
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. Exécutez à nouveau l'application, puis suivez la même procédure pour ajouter et enregistrer les données. Notez que cette fois, l'application revient à l'écran Inventory (Inventaire).

Cette action enregistre les données, mais pour l'instant, vous ne voyez pas les données d'inventaire 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.

Écran de l'application avec une liste d'inventaire vide

10. Afficher le contenu de la base de données à l'aide de l'outil d'inspection

L'outil d'inspection de bases de données vous permet d'inspecter, d'interroger et de modifier les bases de données de votre application pendant son exécution. Il est particulièrement utile pour déboguer des bases de données. Il fonctionne avec SQLite standard et avec des bibliothèques basées sur SQLite, telles que Room. L'outil d'inspection de bases de données fonctionne mieux sur les émulateurs ou appareils utilisant le niveau d'API 26.

  1. Si vous ne l'avez pas déjà fait, exécutez votre application dans un émulateur ou sur un appareil connecté avec le niveau d'API 26 ou supérieur.
  2. Dans Android Studio, sélectionnez View > Tool Windows > App Inspection (Affichage > Fenêtres d'outils > Inspection des applications) dans la barre de menu.
  3. Sélectionnez l'onglet Database Inspector (Outil d'inspection de bases de données).
  4. Dans le volet Database Inspector (Outil d'inspection de bases de données), sélectionnez com.example.inventory dans le menu déroulant si ce n'est pas déjà fait. Le fichier item_database de l'application d'inventaire apparaît dans le volet Databases (Bases de données).

76408bd5e93c3432.png

  1. Développez le nœud correspondant à item_database dans le volet Databases (Bases de données), puis sélectionnez l'article à inspecter. Si le volet Databases (Bases de données) est vide, utilisez l'émulateur pour ajouter des articles à la base de données à l'aide de l'écran Add Item (Ajouter un article).
  2. 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.

9e21d9f7eb426008.png

Félicitations ! Vous avez créé une application qui peut conserver les données sur l'appareil à l'aide de Room. Dans le prochain atelier de programmation, vous ajouterez un objet lazyColumn à votre application pour afficher les articles de la base de données. Vous ajouterez également de nouvelles fonctionnalités à l'application, comme la possibilité de supprimer et de mettre à jour les entités. À bientôt !

11. Télécharger le code de solution

Le code de solution de cet atelier de programmation se trouve dans le dépôt GitHub. Pour télécharger le code de l'atelier de programmation terminé, exécutez les commandes Git suivantes :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout room

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous le souhaitez, vous pouvez consulter le code de solution de cet atelier de programmation sur GitHub.

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

13. En savoir plus

Documentation pour les développeurs Android

Articles de blog

Vidéos

Autres articles et documentation supplémentaire