1. Avant de commencer
La plupart des applications de qualité en production reposent sur des données qui doivent être enregistrées, même après que l'utilisateur a fermé l'application. Par exemple, l'application peut stocker une playlist musicale, une liste de tâches, des tickets de caisse ou des notes de frais, un catalogue de constellations ou un historique de données personnelles. Dans la plupart des cas, pour stocker ces données persistantes, vous avez besoin d'une base de données.
Room est une bibliothèque de persistance qui fait partie d'Android Jetpack. Il s'agit d'une couche d'abstraction qui s'ajoute à une base de données SQLite. SQLite utilise un langage spécialisé (SQL) pour effectuer des opérations sur la base de données. Au lieu d'utiliser directement SQLite, Room simplifie la configuration et les interactions. Room permet également de contrôler le temps de compilation des instructions SQLite.
L'image ci-dessous montre comment cette bibliothèque s'intègre à l'architecture globale recommandée dans cette formation.
Conditions préalables
- Vous savez comment créer une interface utilisateur de base pour une application Android.
- Vous savez utiliser les activités, les fragments et les vues.
- Vous savez naviguer entre les fragments et utiliser Safe Args pour transmettre des données entre fragments.
- Vous maîtrisez les composants d'architecture Android
ViewModel
,LiveData
etFlow
, et vous savez comment utiliserViewModelProvider.Factory
pour instancier les éléments ViewModel. - Vous maîtrisez les principes de base de la simultanéité.
- Vous êtes capable d'utiliser des coroutines pour des tâches de longue durée.
- Vous disposez de connaissances fondamentales sur les bases de données SQL et le langage SQLite.
Points abordés
- Création d'une base de données SQLite et interaction à l'aide de la bibliothèque Room
- Création d'une entité, d'un DAO et de classes de base de données
- Utilisation d'un objet d'accès aux données (DAO, Data Access Object) pour mapper des fonctions Kotlin à des requêtes SQL
Objectifs de l'atelier
- Vous allez créer l'application Inventory, dont le but est d'enregistrer des éléments d'inventaire dans une base de données SQLite.
Ce dont vous avez besoin
- Le code de démarrage de l'application Inventory
- Un ordinateur sur lequel est installé Android Studio
2. Présentation de l'application
Dans cet atelier de programmation, vous allez utiliser une application de démarrage appelée "Inventory" et y ajouter une couche de base de données à l'aide de la bibliothèque Room. La version finale de l'application affiche une liste d'éléments de la base de données d'inventaire à l'aide d'un RecyclerView
. L'utilisateur pourra ajouter, supprimer ou modifier des éléments de la base de données d'inventaire (vous compléterez les fonctionnalités de l'application dans l'atelier de programmation suivant).
Vous trouverez ci-dessous des captures d'écran de la version finale de l'application.
3. Présentation de l'application de démarrage
Télécharger le code de démarrage pour cet atelier de programmation
Cet atelier de programmation fournit un code de démarrage que vous pouvez étendre avec les fonctionnalités qui y sont enseignées. Le code de démarrage peut contenir du code que vous avez déjà vu dans les ateliers de programmation précédents, ainsi que du code inconnu qui sera présenté dans les ateliers suivants.
Si vous utilisez le code de démarrage de GitHub, notez que le nom du dossier est android-basics-kotlin-inventory-app-starter
. Sélectionnez ce dossier lorsque vous ouvrirez le projet dans Android Studio.
Pour obtenir le code de cet atelier de programmation et l'ouvrir dans Android Studio, procédez comme suit :
Obtenir le code
- Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
- Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.
- Dans la boîte de dialogue, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
- Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
- Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.
Ouvrir le projet dans Android Studio
- Lancez Android Studio.
- Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).
Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).
- Dans la boîte de dialogue Import Project (Importer un projet), accédez à l'emplacement du dossier du projet décompressé (qui se trouve probablement dans le dossier Téléchargements).
- Double-cliquez sur le dossier de ce projet.
- Attendez qu'Android Studio ouvre le projet.
- Cliquez sur le bouton Run (Exécuter) pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
- Parcourez les fichiers du projet dans la fenêtre de l'outil Projet pour voir comment l'application est configurée.
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. L'émulateur ou l'appareil connecté doit utiliser le niveau d'API 26 ou supérieur. L'outil d'inspection de bases de données fonctionne mieux sur les émulateurs ou appareils utilisant ce niveau d'API.
- L'application n'affiche aucune donnée d'inventaire. Vous pouvez voir un bouton d'action flottant : il permet d'ajouter des éléments à la base de données.
- Cliquez sur le bouton d'action flottant. L'application ouvre un nouvel écran dans lequel vous pouvez saisir les détails du nouvel élément.
Problèmes avec le code de démarrage
- Sur l'écran Ajouter un élément, saisissez les détails d'un nouvel élément. Appuyez sur Enregistrer. Le fragment d'ajout d'élément ne se ferme pas. Revenez à l'écran précédent à l'aide de la touche de retour arrière du système. Le nouvel élément n'est pas enregistré et ne figure pas sur l'écran d'inventaire. Notez que l'application est incomplète et que le bouton Enregistrer n'est pas implémenté.
Dans cet atelier de programmation, vous allez ajouter le code qui permet à l'application d'enregistrer les détails de l'inventaire dans la base de données SQLite. Pour interagir avec la base de données SQLite, vous utiliserez la bibliothèque de persistance Room.
Tutoriel du code
Le code de démarrage que vous avez téléchargé dispose déjà d'une mise en page. Dans ce parcours, vous allez vous concentrer sur la mise en œuvre de la logique de base de données. Voici une présentation rapide de certains fichiers pour vous aider à commencer.
main_activity.xml
Activité principale qui héberge tous les autres fragments de l'application. La méthode onCreate()
récupère NavController
à partir de NavHostFragment
et configure la barre d'action à utiliser avec NavController
.
item_list_fragment.xml
Le premier écran affiché dans l'application. Il contient principalement un RecyclerView et un bouton d'action flottant. Vous implémenterez le RecyclerView plus tard dans ce parcours.
fragment_add_item.xml
Cette mise en page contient des champs de texte permettant de saisir les détails du nouvel élément d'inventaire à ajouter.
ItemListFragment.kt
Ce fragment contient principalement du code récurrent. Dans la méthode onViewCreated()
, l'écouteur de clics est défini sur le bouton d'action flottant qui permet d'accéder au fragment d'ajout d'élément.
AddItemFragment.kt
Ce fragment permet d'ajouter des éléments à la base de données. La fonction onCreateView()
initialise la variable de liaison tandis que onDestroyView()
masque le clavier avant de détruire le fragment.
4. Principaux composants de Room
Kotlin permet de gérer facilement les données en les organisant en classes. Ces données sont accessibles, et parfois modifiées, à l'aide d'appels de fonction. Toutefois, dans l'univers des bases de données, vous avez besoin de tables et de requêtes pour accéder aux données et les modifier. Les composants de Room que nous allons présenter ici simplifient ces workflows.
Room repose sur trois composants principaux :
- Les entités de données représentent les tables de la base de données de votre application. Elles permettent de mettre à jour les données stockées en lignes dans ces tables, mais aussi de créer des lignes.
- Les objets d'accès aux données (DAO) fournissent des méthodes permettant à votre application de récupérer, de modifier, d'insérer et de supprimer des données dans la base de données.
- La classe Database contient la base de données et constitue le point d'accès principal de la connexion sous-jacente à la base de données de votre application. Cette classe fournit à votre application des instances des DAO associés à cette base de données.
Vous découvrirez et implémenterez ces composants dans la suite de cet atelier de programmation. Le schéma suivant montre comment les composants de Room travaillent ensemble pour interagir avec la base de données.
Ajouter des bibliothèques Room
Dans cette tâche, vous allez ajouter à vos fichiers Gradle les bibliothèques de composants Room dont vous allez avoir besoin.
- Ouvrez le fichier Gradle au niveau du module,
build.gradle (Module: InventoryApp.app)
. Dans le blocdependencies
, ajoutez les dépendances suivantes pour la bibliothèque Room.
// Room implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version"
5. Créer une entité d'article
Une classe d'entité définit une table, et chaque instance de cette classe représente une ligne dans la table de la base de données. La classe d'entité dispose de mappages permettant à Room de présenter les informations de la base de données et d'interagir avec elles. Dans votre application, l'entité va contenir des informations sur les éléments d'inventaire tels que leur nom, leur prix et le stock disponible.
L'annotation @Entity
caractérise une classe d'entité de base de données. Pour contenir les éléments, une table de base de données est créée pour chaque classe d'entité. Chaque champ de l'entité est représenté sous la forme d'une colonne dans la base de données, sauf indication contraire. Pour en savoir plus, consultez la documentation portant sur les entités. Chaque instance d'entité stockée dans la base de données doit avoir une clé primaire. La clé primaire permet d'identifier de manière unique chaque enregistrement/entrée de vos tables de base de données. Une fois attribuée, la clé primaire ne peut plus être modifiée. Elle représente l'objet de l'entité, tant qu'il existe dans la base de données.
Dans cette tâche, vous allez créer une classe d'entité. Définissez des champs pour stocker les informations d'inventaire suivantes pour chaque élément.
- Une valeur
Int
(nombre entier) pour stocker la clé primaire - Une valeur
String
(chaîne de caractères) pour stocker le nom de l'élément - Une valeur
double
(nombre décimal) pour stocker le prix de l'élément - Une valeur
Int
(nombre entier) pour stocker la quantité en stock
- Ouvrez le code de démarrage dans Android Studio.
- Créez un package appelé
data
sous le package de basecom.example.inventory
.
- Dans le package
data
, créez une classe Kotlin appeléeItem
. Cette classe représentera une entité de base de données dans votre application. À l'étape suivante, vous ajouterez les champs correspondants pour stocker les informations sur l'inventaire. - Mettez à jour la définition de la classe
Item
avec le code suivant. Déclarezid
de typeInt
,itemName
de typeString,
itemPrice
de typeDouble
etquantityInStock
de typeInt
comme paramètres pour le constructeur principal. Attribuez la valeur par défaut0
àid
. Il s'agit de la clé primaire, une valeur permettant d'identifier de manière unique chaque enregistrement/entrée de votre tableItem
.
class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
Classes de données
Les classes de données sont principalement utilisées pour stocker des données dans Kotlin. On peut les identifier par le mot clé data
. Les objets de classe de données Kotlin présentent plusieurs avantages. Le compilateur génère automatiquement des utilitaires de comparaison, d'affichage et de copie tels que toString()
, copy()
et equals()
.
Exemple :
// Example data class with 2 properties.
data class User(val first_name: String, val last_name: String){
}
Pour garantir la cohérence et le bon fonctionnement du code généré, les classes de données doivent répondre aux exigences suivantes :
- Le constructeur principal doit comporter au moins un paramètre.
- Tous les paramètres du constructeur principal doivent être marqués comme
val
ouvar
. - Les classes de données ne peuvent pas être
abstract
,open
,sealed
niinner
.
Pour en savoir plus sur les classes de données, consultez la documentation.
- Convertissez la classe
Item
en classe de données en ajoutant le préfixedata
à sa définition.
data class Item(
val id: Int = 0,
val itemName: String,
val itemPrice: Double,
val quantityInStock: Int
)
- Au-dessus de la déclaration de la classe
Item
, annotez la classe de données avec@Entity
. Utilisez l'argumenttableName
pour définiritem
comme nom de table SQLite.
@Entity(tableName = "item")
data class Item(
...
)
- Pour identifier la propriété
id
comme clé primaire, annotez-la avec@PrimaryKey
. Définissez le paramètreautoGenerate
surtrue
pour queRoom
génère l'ID de chaque entité. L'identifiant de chaque élément est ainsi forcément unique.
@Entity(tableName = "item")
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
...
)
- Annotez les propriétés restantes avec
@ColumnInfo
. L'annotationColumnInfo
permet de personnaliser la colonne associée au champ spécifique. Par exemple, lorsque vous utilisez l'argumentname
, vous pouvez spécifier un nom de colonne différent pour le champ plutôt que d'utiliser le nom de la variable. Personnalisez les noms de propriété à l'aide de paramètres, comme indiqué ci-dessous. Cette approche est semblable à l'utilisation detableName
pour spécifier un nom différent à la base de données.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
6. Créer l'élément DAO
Objet d'accès aux données (DAO)
L'objet d'accès aux données (DAO, Data Access Object) est un modèle qui permet de séparer la couche de persistance du reste de l'application en fournissant une interface abstraite. Cette isolation suit le principe de responsabilité unique, que vous avez appris au cours des ateliers de programmation précédents.
Le DAO a pour but de masquer toutes les complexités liées à l'exécution des opérations de base de données à l'aide d'une couche de persistance sous-jacente, séparée du reste de l'application. Cela permet de modifier la couche d'accès aux données indépendamment du code qui utilise ces données.
Dans cette tâche, vous allez définir un objet d'accès aux données (DAO) pour Room. Les DAO sont les principaux composants de Room. Ils définissent l'interface qui accède à la base de données.
Le DAO que vous allez créer sera une interface personnalisée offrant des méthodes pratiques pour interroger (récupérer), insérer, supprimer et modifier des éléments de la base de données. Room générera une implémentation de cette classe au moment de la compilation.
Pour les opérations de base de données courantes, la bibliothèque Room
fournit des annotations pratiques, telles que @Insert
, @Delete
et @Update
. Pour tout le reste, il y a l'annotation @Query
. Vous pouvez écrire n'importe quelle requête prise en charge par SQLite.
En outre, lorsque vous écrivez vos requêtes SQL dans Android Studio, le compilateur vérifie si une erreur de syntaxe est présente.
Pour l'application Inventory, vous devez être en mesure d'effectuer les opérations suivantes :
- Insérer ou ajouter un élément.
- Modifier un élément existant : son nom, son prix et sa quantité.
- Obtenir un élément spécifique en fonction de sa clé primaire,
id
. - Obtenir tous les éléments afin de pouvoir les afficher.
- Supprimer une entrée de la base de données.
Maintenant, implémentez l'élément DAO dans votre application :
- Dans le package
data
, créez la classe KotlinItemDao.kt
. - Remplacez la définition de classe par
interface
et ajoutez l'annotation@Dao
.
@Dao
interface ItemDao {
}
- Dans le corps de l'interface, ajoutez une annotation
@Insert
. En dessous celle-ci, ajoutez une fonctioninsert()
qui reçoit une instance de la classeEntity
appeléeitem
comme argument. Les opérations sur la base de données peuvent prendre beaucoup de temps. Elles doivent donc s'exécuter sur un thread distinct. Transformez cette fonction une fonction de suspension afin qu'elle puisse être appelée à partir d'une coroutine.
@Insert
suspend fun insert(item: Item)
- Ajoutez un argument
OnConflict
et attribuez-lui la valeurOnConflictStrategy.
IGNORE
. L'argumentOnConflict
indique à Room la marche à suivre en cas de conflit. La stratégieOnConflictStrategy.
IGNORE
ignore le nouvel élément si sa clé primaire figure déjà dans la base de données. Pour en savoir plus sur les stratégies de gestion de conflit disponibles, consultez la documentation.
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
Room
va maintenant générer tout le code nécessaire pour insérer l'élément item
dans la base de données. Lorsque vous appelez insert()
à partir de votre code Kotlin, Room
exécute une requête SQL pour insérer l'entité dans la base de données. Souvenez-vous que vous pouvez attribuer le nom de votre choix à la fonction. Elle ne doit pas nécessairement s'appeler insert()
.
- Ajoutez une annotation
@Update
avec une fonctionupdate()
pour un élémentitem
. L'entité mise à jour a la même clé que l'entité transmise. Vous pouvez mettre à jour tout ou partie des autres propriétés de l'entité. Comme pour la méthodeinsert()
, utilisezsuspend
(suspension) sur la méthodeupdate()
suivante.
@Update
suspend fun update(item: Item)
- Ajoutez l'annotation
@Delete
avec une fonctiondelete()
pour supprimer un ou des éléments. Faites-en une méthode de suspension. L'annotation@Delete
supprime un élément ou une liste d'éléments. Gardez à l'esprit que vous devez transmettre la ou les entités à supprimer. Si vous ne disposez pas de ces entités, vous devrez peut-être les récupérer avant d'appeler la fonctiondelete()
.
@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 élément spécifique de la table en fonction de l'
id
donné. Vous ajouterez ensuite une annotation Room et utiliserez une version modifiée de la requête ci-dessous aux étapes suivantes. Dans la suite de cet atelier de programmation, vous allez également remplacer cette méthode par un DAO à l'aide de Room. - Sélectionnez toutes les colonnes de
item
. - Avec
WHERE
, spécifiez unid
qui correspond à une valeur spécifique.
Exemple :
SELECT * from item WHERE id = 1
- Modifiez la requête SQL ci-dessus pour y ajouter une annotation Room et un argument. Ajoutez l'annotation
@Query
et indiquez-y la requête en tant que paramètre de chaîne. Ajoutez un paramètreString
à@Query
. Il s'agit d'une requête SQLite permettant de récupérer un élément dans la table. - Sélectionnez toutes les colonnes de
item
. - Avec
WHERE
, l'id
correspond à l'argument :id
. Remarquez bien:id
. Utilisez le caractère deux-points, ce qui permet à la requête de faire référence à des arguments dans la fonction.
@Query("SELECT * from item WHERE id = :id")
- Sous l'annotation
@Query
, ajoutez une fonctiongetItem()
qui reçoit un argumentInt
et renvoieFlow<Item>
.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
Utiliser Flow
ou LiveData
comme type renvoyé vous permettra d'être averti à chaque fois que des éléments de la base de données sont modifiés. Il est recommandé d'utiliser Flow
dans la couche de persistance. Room
maintient ce Flow
à jour pour vous, ce qui signifie que vous n'avez besoin d'obtenir les données de façon explicite qu'une seule fois. Ce système est particulièrement utile pour mettre à jour la liste d'inventaire, que vous implémenterez dans l'atelier de programmation suivant. Étant donné que le type renvoyé est Flow
, Room exécute également la requête sur le thread d'arrière-plan. Vous n'avez pas besoin de la définir explicitement comme une fonction suspend
et de l'appeler dans le cadre d'une coroutine.
Il est possible que vous ayez besoin d'importer Flow
depuis kotlinx.coroutines.flow.Flow
.
- Ajoutez une annotation
@Query
avec une fonctiongetItems()
: - Demandez à la requête SQLite de renvoyer toutes les colonnes de la table
item
, puis d'effectuer un tri par ordre croissant. - Demandez à
getItems()
de renvoyer la liste des entitésItem
en tant queFlow
.Room
maintient ceFlow
à jour pour vous, ce qui signifie que vous n'avez besoin d'obtenir les données de façon explicite qu'une seule fois.
@Query("SELECT * from item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
- Bien qu'aucune modification ne soit visible, exécutez votre application pour vous assurer qu'elle ne comporte pas d'erreurs.
7. Créer une instance de base de données
Dans cette tâche, vous allez créer RoomDatabase
, qui utilise les Entity
et les DAO créés au cours de la tâche précédente. La classe de base de données définit la liste des entités et des objets d'accès aux données. Il s'agit également du point d'accès principal de la connexion sous-jacente.
La classe Database
fournit à votre application les instances des DAO que vous avez définis. L'application peut ensuite utiliser ces DAO pour récupérer des données à partir de la base de données en tant qu'instances des objets d'entité associés. L'application peut également utiliser les entités de données définies pour modifier des lignes dans les tables correspondantes ou pour en créer.
Vous devez créer une classe RoomDatabase
abstraite, annotée avec @Database
. Cette classe comporte une méthode qui crée une instance de RoomDatabase
si elle n'existe pas ou qui renvoie l'instance existante le cas échéant.
Voici le processus général pour obtenir l'instance RoomDatabase
:
- Créez une classe
public abstract
qui é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, répertoriez les entités de la base de données et définissez le numéro de version. - Définissez une méthode ou une propriété abstraite qui renvoie une instance
ItemDao
.Room
générera alors l'implémentation automatiquement. - Vous n'avez besoin que d'une seule instance de
RoomDatabase
pour l'ensemble de l'application. Vous devez donc faire en sorte 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 Kotlin :ItemRoomDatabase.kt
. - Dans le fichier
ItemRoomDatabase.kt
, définissez la classeItemRoomDatabase
en tant que classeabstract
(abstraite) qui étendRoomDatabase
. Annotez la classe avec@Database
. L'erreur qui indique que des paramètres sont manquants sera corrigée à l'étape suivante.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
- 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 incrémenter 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)
- La base de données doit connaître le DAO. Dans le corps de la classe, déclarez une fonction abstraite qui renvoie
ItemDao
. Vous pouvez avoir plusieurs DAO.
abstract fun itemDao(): ItemDao
- Sous la fonction abstraite, définissez un objet
companion
(compagnon). L'objet compagnon permet d'accéder aux méthodes de création ou d'obtention de la base de données en utilisant le nom de classe comme qualificatif.
companion object {}
- Dans l'objet
companion
, déclarez uneINSTANCE
, une variable privée pouvant être nulle, avant de l'initialiser en lui attribuant la valeurnull
. La variableINSTANCE
conservera une référence à la base de données, si vous en avez créé une. Ainsi, une seule instance de la base de données est ouverte à tout moment. Cela est particulièrement intéressant, car les bases de données ont besoin de beaucoup de ressources, que ce soit pour leur création ou pour leur gestion.
Annotez la variable INSTANCE
avec @Volatile
. La valeur d'une variable volatile ne sera jamais mise en cache, et toutes les écritures et lectures seront effectuées vers et depuis la mémoire principale. Cela permet de s'assurer que la valeur de la variable INSTANCE
est toujours à jour et identique pour tous les threads d'exécution. En d'autres termes, les modifications apportées à INSTANCE
par un thread sont immédiatement visibles par tous les autres.
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
- En dessous de
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 typeItemRoomDatabase
. Une erreur s'affiche, cargetDatabase()
ne renvoie rien pour le moment.
fun getDatabase(context: Context): ItemRoomDatabase {}
- Il est possible que différents threads se retrouvent dans une condition de concurrence et demandent une instance de base de données en même temps, ce qui a pour effet de dupliquer la base de données. En encapsulant le code pour placer la base de données dans un bloc
synchronized
, vous vous assurez qu'un seul thread d'exécution à la fois peut y accéder. Ainsi, vous avez la certitude que la base de données n'est initialisée qu'une seule fois.
Dans getDatabase()
, renvoyez la variable INSTANCE
ou, si sa valeur est null, initialisez-la dans un bloc synchronized{}
. Pour ce faire, utilisez l'opérateur Elvis (?:
). Transmettez this
à l'objet compagnon que vous souhaitez verrouiller dans le bloc de fonction. Vous corrigerez l'erreur au cours des étapes suivantes.
return INSTANCE ?: synchronized(this) { }
- Dans le bloc synchronisé, créez une variable d'instance
val
et utilisez l'outil de création de base de données pour l'obtenir. Des erreurs continueront d'apparaître, vous les corrigerez au cours des étapes suivantes.
val instance = Room.databaseBuilder()
- À la fin du bloc
synchronized
, renvoyezinstance
.
return instance
- Dans le bloc
synchronized
, initialisez la variableinstance
et utilisez l'outil de création de base de données pour l'obtenir. Transmettez àRoom.databaseBuilder()
le contexte de l'application, la classe de base de données et un nom pour celle-ci,item_database
.
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
Android Studio générera une erreur de non-correspondance de type. Pour supprimer cette erreur, vous devrez ajouter une stratégie de migration et build()
au cours des étapes suivantes.
- Ajoutez la stratégie de migration requise au créateur. Utilisez
.fallbackToDestructiveMigration()
.
Normalement, vous devez accompagner toute stratégie de migration d'un objet de migration, au cas où le schéma viendrait à être modifié. Un objet de migration définit la façon dont vous prenez toutes les lignes de l'ancien schéma pour les rendre compatibles avec le nouveau, de sorte qu'aucune donnée ne soit perdue. Les principes de la migration dépassent le cadre de cet atelier de programmation. Une solution simple consiste à détruire et à recréer la base de données, ce qui entraîne une perte de données.
.fallbackToDestructiveMigration()
- Pour créer l'instance de base de données, appelez
.build()
. Les erreurs affichées par Android Studio devraient être supprimées.
.build()
- Dans le bloc
synchronized
, attribuezINSTANCE = instance
.
INSTANCE = instance
- À la fin du bloc
synchronized
, renvoyezinstance
. Votre code, une fois fini, doit ressembler à ceci :
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
fun getDatabase(context: Context): ItemRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
- Compilez votre code pour vous assurer qu'il ne comporte pas d'erreur.
Implémenter la classe d'application
Dans cette tâche, vous allez instancier l'instance de base de données dans la classe d'application.
- Ouvrez
InventoryApplication.kt
, créez unval
nommédatabase
de typeItemRoomDatabase
. Instanciezdatabase
en appelantgetDatabase()
surItemRoomDatabase
et en transmettant le contexte. Utilisez le déléguélazy
pour que l'instancedatabase
soit créée par nécessité, lorsque vous avez besoin de la référence ou y accédez pour la première fois (plutôt que lorsque l'application démarre). Cette opération crée la base de données (la base de données physique sur le disque) lors du premier accès.
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase
class InventoryApplication : Application(){
val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}
Vous utiliserez l'instance database
lorsque vous créerez une instance ViewModel, dans les étapes suivantes de l'atelier de programmation.
Vous disposez désormais de tous les éléments nécessaires à l'utilisation de Room. Ce code peut être compilé et exécuté, mais vous n'avez aucun moyen de savoir s'il fonctionne réellement. C'est donc le moment d'ajouter un élément à votre base de données d'inventaire pour la tester. Pour ce faire, vous avez besoin d'un ViewModel
afin de communiquer avec la base de données.
8. Ajouter un ViewModel
Jusqu'à présent, vous avez créé une base de données. Les classes de l'interface utilisateur, pour leur part, faisaient partie du code de démarrage. Pour enregistrer les données temporaires de l'application et accéder à la base de données, vous avez besoin d'un ViewModel. Le ViewModel de l'application Inventory interagira avec la base de données à l'aide du DAO et fournira des données à l'UI. Toutes les opérations de base de données devront être exécutées hors du thread UI principal. Pour ce faire, vous allez devoir utiliser des coroutines et viewModelScope
.
Créer un ViewModel pour Inventory
- Dans le package
com.example.inventory
, créez un fichier de classe Kotlin,InventoryViewModel.kt
. - Étendez la classe
InventoryViewModel
à partir deViewModel
. Transmettez l'objetItemDao
en tant que paramètre au constructeur par défaut.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
- À la fin du fichier
InventoryViewModel.kt
, en dehors de la classe, ajoutez la classeInventoryViewModelFactory
pour instancierInventoryViewModel
. Transmettez le même paramètre de constructeur queInventoryViewModel
, à savoir l'instanceItemDao
. Étendez la classe à partir deViewModelProvider.Factory
. Au cours de l'étape suivante, vous corrigerez l'erreur concernant les méthodes non implémentées.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
- Cliquez sur l'ampoule rouge et sélectionnez Implémenter les membres. Vous pouvez également forcer la méthode
create()
dans la classeViewModelProvider.Factory
, comme suit. Elle accepte tous les types de classe comme argument et renvoieViewModel
.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
TODO("Not yet implemented")
}
- Implémentez la méthode
create()
. Vérifiez simodelClass
est identique à la classeInventoryViewModel
et renvoyez une instance de celle-ci. Dans le cas contraire, générez une exception.
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
Remplir le ViewModel
Dans cette tâche, vous allez remplir la classe InventoryViewModel
pour ajouter des données d'inventaire à la base de données. Dans l'application Inventory, observez l'entité Item
et l'écran Ajouter un élément.
@Entity
data class Item(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "name")
val itemName: String,
@ColumnInfo(name = "price")
val itemPrice: Double,
@ColumnInfo(name = "quantity")
val quantityInStock: Int
)
Pour ajouter une entité à la base de données, vous devez indiquer le nom, le prix et le stock de cet élément. Plus tard dans l'atelier de programmation, vous utiliserez l'écran Ajouter un élément pour recevoir ces informations de la part de l'utilisateur. Pour la tâche actuelle, cependant, nous allons nous contenter d'utiliser trois chaînes en tant qu'entrées pour ViewModel, les convertir en instance d'entité Item
et les enregistrer dans la base de données à l'aide de l'instance ItemDao
. Il est temps d'implémenter tout ceci.
- Dans la classe
InventoryViewModel
, ajoutez une fonctionprivate
(privée) appeléeinsertItem()
, qui reçoit un objetItem
et ajoute des informations à la base de données de manière non bloquante.
private fun insertItem(item: Item) {
}
- Pour interagir avec la base de données hors du thread principal, démarrez une coroutine et appelez-y la méthode DAO. Dans la méthode
insertItem()
, utilisezviewModelScope.launch
pour démarrer une coroutine dansViewModelScope
. Dans la fonction de lancement, appelez la fonction de suspensioninsert()
suritemDao
en transmettantitem
.ViewModelScope
est une propriété d'extension de la classeViewModel
qui annule automatiquement ses coroutines enfants lorsqueViewModel
est détruite.
private fun insertItem(item: Item) {
viewModelScope.launch {
itemDao.insert(item)
}
}
Importez kotlinx.coroutines.launch,
androidx.lifecycle.
viewModelScope
com.example.inventory.data.Item
, si cette importation ne s'est pas faite automatiquement.
- Dans la classe
InventoryViewModel
, ajoutez une autre fonction privée qui reçoit trois chaînes de caractères et renvoie une instanceItem
.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
return Item(
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
- Toujours dans la classe
InventoryViewModel
, ajoutez une fonction publique appeléeaddNewItem()
qui reçoit trois chaînes de caractères (les informations sur l'élément à ajouter). Transmettez ces chaînes à la fonctiongetNewItemEntry()
, puis attribuez la valeur renvoyée à un val nomménewItem
. AppelezinsertItem()
pour transmettrenewItem
afin d'ajouter l'entité à la base de données. Elle sera appelée à partir du fragment d'UI pour ajouter à la base de données les informations sur les éléments.
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
insertItem(newItem)
}
Notez que vous n'avez pas utilisé viewModelScope.launch
pour addNewItem()
, mais que vous en avez besoin ci-dessus dans insertItem()
, lorsque vous appelez une méthode DAO. En effet, les fonctions de suspension ne peuvent être appelées qu'à partir d'une coroutine ou d'une autre fonction de suspension. Or, la fonction itemDao.insert(item)
est justement une fonction de suspension.
Vous avez ajouté toutes les fonctions requises pour ajouter des entités à la base de données. Dans la tâche suivante, vous allez modifier le fragment Ajouter un élément pour utiliser les fonctions ci-dessus.
9. Modifier AddItemFragment
- Dans
AddItemFragment.kt
, au début de la classeAddItemFragment
, créez un élémentprivate val
appeléviewModel
, de typeInventoryViewModel
. Utilisez le délégué de propriété Kotlinby activityViewModels()
pour appliquer votreViewModel
sur plusieurs fragments. Vous corrigerez l'erreur à l'étape suivante.
private val viewModel: InventoryViewModel by activityViewModels {
}
- Dans le lambda, appelez le constructeur
InventoryViewModelFactory()
et transmettez l'instanceItemDao
. Utilisez l'instancedatabase
que vous avez créée dans l'une des tâches précédentes pour appeler le constructeuritemDao
.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database
.itemDao()
)
}
- Sous la définition
viewModel
, créez unlateinit var
nomméitem
, de typeItem
.
lateinit var item: Item
- L'écran Ajouter un élément contient trois champs de texte permettant à l'utilisateur de transmettre les informations relatives au nouvel élément. Au cours de cette étape, vous allez ajouter une fonction pour vérifier que les champs de texte ne sont pas vides. Vous utiliserez cette fonction pour vérifier les entrées utilisateur avant d'ajouter ou de modifier l'entité dans la base de données. Cette validation doit être effectuée dans
ViewModel
et non dans le fragment. Dans la classeInventoryViewModel
, ajoutez la fonctionpublic
(publique) suivante, appeléeisEntryValid()
.
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
return false
}
return true
}
- Dans
AddItemFragment.kt
, sous la fonctiononCreateView()
, créez une fonctionprivate
appeléeisEntryValid()
, qui renvoie unBoolean
(valeur booléenne). L'erreur qui indique que la valeur renvoyée est manquante sera corrigée à l'étape suivante.
private fun isEntryValid(): Boolean {
}
- Dans la classe
AddItemFragment
, implémentez la fonctionisEntryValid()
. Appelez la fonctionisEntryValid()
sur l'instanceviewModel
en transmettant le texte issu des champs. Renvoyez la valeur de la fonctionviewModel.isEntryValid()
.
private fun isEntryValid(): Boolean {
return viewModel.isEntryValid(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
}
- Dans la classe
AddItemFragment
, sous la fonctionisEntryValid()
, ajoutez une autre fonctionprivate
(privée) appeléeaddNewItem()
. Elle ne comporte pas de paramètre et ne renvoie rien. Dans cette fonction, appelezisEntryValid()
dans la conditionif
.
private fun addNewItem() {
if (isEntryValid()) {
}
}
- Dans le bloc
if
, appelez la méthodeaddNewItem()
sur l'instanceviewModel
. Transmettez les informations sur l'élément saisies par l'utilisateur et utilisez l'instancebinding
pour les lire.
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
}
- Sous le bloc
if
, créez uneaction
val
pour revenir àItemListFragment
. AppelezfindNavController
().navigate()
en transmettant votreaction
.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
Importez androidx.navigation.fragment.findNavController.
- La méthode terminée doit se présenter comme suit.
private fun addNewItem() {
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString(),
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
- Pour unifier le tout, ajoutez un gestionnaire de clics au bouton Save (Enregistrer). Dans la classe
AddItemFragment
, au-dessus de la fonctiononDestroyView()
, forcezonViewCreated()
. - Dans la fonction
onViewCreated()
, ajoutez un gestionnaire de clics au bouton d'enregistrement et appelezaddNewItem()
à partir de celui-ci.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.saveAction.setOnClickListener {
addNewItem()
}
}
- Compilez et exécutez votre application. Appuyez sur le bouton d'action flottant +. Sur l'écran Ajouter un élément, ajoutez les détails du nouvel élément et appuyez sur Enregistrer. Cette action enregistre les données, mais pour l'instant, vous ne voyez rien dans l'application. Dans la tâche suivante, vous allez utiliser l'outil d'inspection de bases de données pour afficher les données que vous avez enregistrées.
Afficher la base de données à l'aide de l'outil d'inspection
- Exécutez votre application sur un émulateur ou un appareil connecté avec un niveau d'API 26 ou supérieur, si vous ne l'avez pas déjà fait. L'outil d'inspection de bases de données fonctionne mieux sur les émulateurs ou appareils utilisant ce niveau d'API.
- Dans la barre de menu d'Android Studio, sélectionnez Affichage > Fenêtres d'outil > Outil d'inspection de bases de données.
- Dans le menu déroulant du volet Outil d'inspection de bases de données, sélectionnez
com.example.inventory
. - Le fichier item_database de l'application Inventory apparaît dans le volet Bases de données. Développez le nœud du fichier item_database et sélectionnez Élément pour l'inspecter. Si le volet Bases de données est vide, utilisez votre émulateur pour ajouter des éléments à la base de données à l'aide de l'écran Ajouter un élément.
- 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 avec des données persistantes à l'aide de Room. Dans le prochain atelier de programmation, vous ajouterez un RecyclerView
à votre application pour afficher les éléments de la base de données. Vous ajouterez également de nouvelles fonctionnalités à l'application, comme la suppression et la modification d'entités. À bientôt !
10. Code de solution
Le code de la solution de cet atelier de programmation se trouve dans le dépôt et la branche GitHub indiqués ci-dessous.
Pour obtenir le code de cet atelier de programmation et l'ouvrir dans Android Studio, procédez comme suit :
Obtenir le code
- Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
- Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.
- Dans la boîte de dialogue, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
- Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
- Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.
Ouvrir le projet dans Android Studio
- Lancez Android Studio.
- Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).
Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).
- Dans la boîte de dialogue Import Project (Importer un projet), accédez à l'emplacement du dossier du projet décompressé (qui se trouve probablement dans le dossier Téléchargements).
- Double-cliquez sur le dossier de ce projet.
- Attendez qu'Android Studio ouvre le projet.
- Cliquez sur le bouton Run (Exécuter) pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
- Parcourez les fichiers du projet dans la fenêtre de l'outil Projet pour voir comment l'application est configurée.
11. Résumé
- Définissez vos tables en tant que classes de données annotées avec
@Entity
. Définissez les propriétés annotées avec@ColumnInfo
en tant que colonnes dans les tables. - Définissez un objet d'accès aux données (DAO) en tant qu'interface annotée avec
@Dao
. Le DAO mappe les fonctions Kotlin aux requêtes de base de données. - Utilisez les annotations pour définir les fonctions
@Insert
,@Delete
et@Update
. - Pour toutes les autres requêtes, utilisez l'annotation
@Query
avec une chaîne de requête SQLite comme paramètre. - Utilisez l'outil d'inspection de bases de données pour afficher les données enregistrées dans la base de données Android SQLite.
12. En savoir plus
Documentation pour les développeurs Android
- 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