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.
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
etLazyColumn
. - 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 utiliserViewModelProvider.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.
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
- Dans Android Studio, ouvrez le projet contenant le code de démarrage.
- 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.
- Notez que l'application n'affiche aucune donnée d'inventaire.
- 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.
Problèmes avec le code de démarrage
- Sur l'écran Add Item (Ajouter un article), saisissez les détails de l'article, tels que son nom, son prix et sa quantité.
- 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é.
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.
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.
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.
Ajouter des dépendances Room
Dans cette tâche, vous allez ajouter les bibliothèques de composants Room requises dans vos fichiers Gradle.
- Ouvrez le fichier Gradle
build.gradle.kts (Module: InventoryApp.app)
au niveau du module. - 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é.
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.
- Ouvrez le code de démarrage dans Android Studio.
- Ouvrez le package
data
sous le package de basecom.example.inventory
. - Dans le package
data
, ouvrez la classe KotlinItem
, 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
ouvar
. - Les classes de données ne peuvent pas être
abstract
,open
nisealed
.
Pour en savoir plus, consultez la documentation sur les classes de données.
- Ajoutez le mot clé
data
comme préfixe à la définition de la classeItem
afin de la convertir en classe de données.
data class Item(
val id: Int,
val name: String,
val price: Double,
val quantity: Int
)
- Au-dessus de la déclaration de la classe
Item
, annotez la classe de données avec@Entity
. Utilisez l'argumenttableName
pour définiritems
comme nom de table SQLite.
import androidx.room.Entity
@Entity(tableName = "items")
data class Item(
...
)
- 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 tableItem
.
import androidx.room.PrimaryKey
@Entity(tableName = "items")
data class Item(
@PrimaryKey
val id: Int,
...
)
- Attribuez à
id
la valeur par défaut0
, nécessaire pour queid
puisse générer automatiquement des valeursid
. - Ajoutez le paramètre
autoGenerate
à l'annotation@PrimaryKey
pour spécifier si la colonne de clé primaire doit être générée automatiquement. SiautoGenerate
est défini surtrue
, 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.
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
Pour implémenter le DAO dans votre application, procédez comme suit :
- Dans le package
data
, créez l'interface KotlinItemDao.kt
.
- Annotez l'interface
ItemDao
avec@Dao
.
import androidx.room.Dao
@Dao
interface ItemDao {
}
- Dans le corps de l'interface, ajoutez une annotation
@Insert
. - Sous
@Insert
, ajoutez une fonctioninsert()
qui reçoit une instance de la classeEntity
appeléeitem
comme argument. - 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).
- Ajoutez un argument
onConflict
et attribuez-lui la valeurOnConflictStrategy.
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.
- Ajoutez une fonction avec une annotation
@Update
qui utilise un paramètreItem
.
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é.
- 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.
- É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 depuisitems
, oùid
correspond à une valeur spécifique etid
est un identifiant unique.
Exemple :
// Example, no need to copy over
SELECT * from items WHERE id = 1
- Ajoutez une annotation
@Query
. - Utilisez la requête SQLite de l'étape précédente en tant que paramètre de chaîne dans l'annotation
@Query
. - 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")
- Après l'annotation
@Query
, ajoutez une fonctiongetItem()
qui accepte un argumentInt
et renvoie unFlow<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.
- Ajoutez une annotation
@Query
avec une fonctiongetAllItems()
. - Demandez à la requête SQLite de renvoyer toutes les colonnes de la table
item
, puis d'effectuer un tri par ordre croissant. - Demandez à
getAllItems()
de renvoyer la liste des entitésItem
en tant queFlow
.Room
veille automatiquement à ce queFlow
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>>
}
- 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 étendRoomDatabase
. La nouvelle classe abstraite que vous avez définie fait office de conteneur de base de données. Elle est abstraite, carRoom
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 queRoomDatabase
soit un singleton. - Avec
Room
, utilisezRoom.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
- Dans le package
data
, créez une classe KotlinInventoryDatabase.kt
. - Dans le fichier
InventoryDatabase.kt
, définissez la classeInventoryDatabase
en tant que classeabstract
(abstraite) qui étendRoomDatabase
. - 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.
- Spécifiez
Item
en tant que seule classe contenant la liste d'entities
(entités). - Définissez
version
sur1
. Chaque fois que vous modifiez le schéma de la table de base de données, vous devez augmenter le numéro de version. - Définissez
exportSchema
surfalse
pour ne pas conserver les sauvegardes de l'historique des versions de schéma.
@Database(entities = [Item::class], version = 1, exportSchema = false)
- 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
- 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 {}
- Dans l'objet
companion
, déclarez uneInstance
, une variable privée pouvant être nulle, avant de l'initialiser en lui attribuant la valeurnull
.
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.
- 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
- Sous
Instance
, tout en restant dans l'objetcompanion
, définissez une méthodegetDatabase()
avec un paramètreContext
, dont l'outil de création de base de données aura besoin. - Renvoyez un type
InventoryDatabase
. Un message d'erreur s'affiche, cargetDatabase()
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.
- Dans
getDatabase()
, renvoyez la variableInstance
ou, si sa valeurInstance
est null, initialisez-la dans un blocsynchronized{}
. Pour ce faire, utilisez l'opérateur Elvis (?:
). - Transmettez
this
, l'objet associé. Vous corrigerez l'erreur plus tard.
return Instance ?: synchronized(this) { }
- 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()
- 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.
- Ajoutez la stratégie de migration requise au compilateur. Utilisez
.
fallbackToDestructiveMigration()
.
.fallbackToDestructiveMigration()
- Pour créer l'instance de base de données, appelez
.build()
. Cette action supprime les erreurs Android Studio.
.build()
- Après
build()
, ajoutez un blocalso
et attribuezInstance = it
pour conserver une référence à l'instance de base de données qui vient d'être créée.
.also { Instance = it }
- À la fin du bloc
synchronized
, renvoyezinstance
. 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 }
}
}
}
}
- 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.
- Ouvrez le fichier
ItemsRepository.kt
sous le packagedata
. - 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)
}
- Ouvrez le fichier
OfflineItemsRepository.kt
sous le packagedata
. - Transmettez un paramètre constructeur de type
ItemDao
.
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
- Dans la classe
OfflineItemsRepository
, remplacez les fonctions définies dans l'interfaceItemsRepository
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
.
- Ouvrez le fichier
AppContainer.kt
sous le packagedata
. - Transmettez l'instance
ItemDao()
au constructeurOfflineItemsRepository
. - Instanciez l'instance de base de données. Pour ce faire, appelez
getDatabase()
au niveau de la classeInventoryDatabase
en transmettant le contexte, puis appelez.itemDao()
pour créer l'instance deDao
.
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 ViewModel
s 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'UIItemUiState
en type d'entitéItem
. - La fonction d'extension
Item.toItemUiState()
convertit l'objet d'entité RoomItem
en type d'état d'UIItemUiState
. - La fonction d'extension
Item.toItemDetails()
convertit l'objet d'entité RoomItem
enItemDetails
.
// 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).
- Notez la fonction privée
validateInput()
dans la classeItemEntryViewModel
.
// 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.
- Ouvrez la classe
ItemEntryViewModel
et ajoutez un paramètre constructeur par défautprivate
de typeItemsRepository
.
import com.example.inventory.data.ItemsRepository
class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
- Mettez à jour
initializer
pour le modèle de vue d'entrée d'article dansui/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)
}
//...
}
}
- Accédez au fichier
ItemEntryViewModel.kt
, et à la fin de la classeItemEntryViewModel
, ajoutez une fonction de suspension appeléesaveItem()
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() {
}
- Dans la fonction, vérifiez si
itemUiState
est valide et convertissez-le en typeItem
pour que Room puisse comprendre les données. - Appelez
insertItem()
suritemsRepository
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()
- Dans le fichier
ui/item/ItemEntryScreen.kt
, le composableItemEntryBody()
est partiellement implémenté pour vous dans le code de démarrage. Examinez le composableItemEntryBody()
dans l'appel de fonctionItemEntryScreen()
.
// 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()
)
- 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).
- Examinez l'implémentation de la fonction composable
ItemInputForm()
et notez le paramètre de la fonctiononValueChange
. La valeuritemDetails
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.
- Dans
ItemEntryScreen.kt
, dans la fonction composableItemEntryScreen
, créez un attributval
nommécoroutineScope
avec la fonction composablerememberCoroutineScope()
.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Mettez à jour l'appel de la fonction
ItemEntryBody
()
et lancez une coroutine dans le lambdaonSaveClick
.
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
}
},
modifier = modifier.padding(innerPadding)
)
- Examinez l'implémentation de la fonction
saveItem()
dans le fichierItemEntryViewModel.kt
pour vérifier siitemUiState
est valide, en convertissantitemUiState
en typeItem
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())
}
}
- Dans
ItemEntryScreen.kt
, dans la fonction composableItemEntryScreen
, dans la coroutine, appelezviewModel.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.
- Compilez et exécutez votre application.
- Appuyez sur le bouton d'action flottant +.
- 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).
- Dans le lambda
onSaveClick
, ajoutez un appel ànavigateBack()
après l'appel àviewModel.saveItem()
pour revenir à l'écran précédent. Votre fonctionItemEntryBody()
devrait ressembler au code suivant :
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.saveItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- 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.
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.
- 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.
- Dans Android Studio, sélectionnez View > Tool Windows > App Inspection (Affichage > Fenêtres d'outils > Inspection des applications) dans la barre de menu.
- Sélectionnez l'onglet Database Inspector (Outil d'inspection de bases de données).
- 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).
- 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).
- 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.
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
- Enregistrer des données dans une base de données locale à l'aide de Room
- androidx.room
- Déboguer votre base de données avec l'outil d'inspection de bases de données
Articles de blog
- 7 Pro-tips for Room (7 conseils d'expert pour Room)
- The one and only object. Kotlin Vocabulary (Un seul et unique objet : le vocabulaire Kotlin)
Vidéos
Autres articles et documentation supplémentaire