Présentation de Room et de Flow

1. Avant de commencer

Dans l'atelier de programmation précédent, vous avez découvert les principes fondamentaux des bases de données relationnelles. Vous avez également appris à lire et à écrire des données à l'aide des commandes SELECT, INSERT, UPDATE et DELETE en SQL. La maîtrise des bases de données relationnelles est une compétence essentielle que vous devrez acquérir tout au long de votre parcours de formation en programmation. Il est également crucial de connaître le fonctionnement de ces bases de données pour assurer la persistance des données dans une application Android, ce que vous apprendrez dans ce cours.

Pour utiliser facilement une base de données dans une application Android, vous pouvez utiliser une bibliothèque appelée Room. Room est ce qu'on appelle une bibliothèque ORM (Object Relational Mapping), qui mappe les tables d'une base de données relationnelle aux objets utilisables en code Kotlin. Dans ce cours, vous allez vous concentrer sur la lecture des données. À l'aide d'une base de données préremplie, vous allez charger des données à partir d'une table contenant les heures d'arrivée d'une ligne de bus et les présenter dans un élément RecyclerView.

70c597851eba9518.png

Tout au long de ce processus, vous découvrirez les bases de l'utilisation de Room, notamment la classe de base de données, le DAO (objet d'accès aux données), les entités et les modèles d'affichage. Nous vous présenterons également la classe ListAdapter, qui est une autre façon de présenter des données dans un élément RecyclerView, et le flux, une fonctionnalité du langage Kotlin semblable à LiveData. Elle permettra à votre UI de réagir aux modifications de la base de données.

Conditions préalables

  • Vous maîtrisez la programmation orientée objet et l'utilisation des classes, des objets et de l'héritage en Kotlin.
  • Vous disposez de notions fondamentales sur les bases de données relationnelles et le langage SQL, lesquelles sont reprises dans l'atelier de programmation sur les principes de base de SQL.
  • Vous savez utiliser des coroutines Kotlin.

Points abordés

À la fin de cette session, vous devriez pouvoir :

  • représenter des tables de base de données en tant qu'objets Kotlin (entités) ;
  • définir la classe de base de données pour utiliser Room dans l'application et préremplir une base de données à partir d'un fichier ;
  • définir la classe DAO et utiliser des requêtes SQL pour accéder à la base de données à partir du code Kotlin ;
  • définir un modèle d'affichage pour permettre à l'interface utilisateur d'interagir avec le DAO ;
  • utiliser ListAdapter avec un élément RecyclerView ;
  • manipuler les bases du flux Kotlin et l'utiliser de sorte que l'interface utilisateur réagisse aux modifications des données sous-jacentes.

Objectifs de l'atelier

  • Lire les données d'une base de données préremplie à l'aide de Room et les présenter dans un élément RecyclerView (vue recycleur) dans une simple application d'horaires de bus.

2. Premiers pas

L'application que vous allez utiliser dans cet atelier de programmation s'appelle "Bus Schedule". Elle recense une liste d'arrêts de bus avec des heures d'arrivée, dans l'ordre chronologique.

70c597851eba9518.png

Si vous appuyez sur une ligne sur le premier écran, un nouvel écran s'ouvre et affiche uniquement les heures d'arrivée pour l'arrêt de bus sélectionné.

f477c0942746e584.png

La liste des arrêts de bus et les horaires associés proviennent d'une base de données préinstallée avec l'application. Toutefois, dans son état actuel, rien ne s'affiche lorsque l'application est exécutée pour la première fois. Votre tâche consiste à intégrer Room afin que l'application affiche la base de données préremplie avec les heures d'arrivée.

  1. Accédez à la page du dépôt GitHub fournie pour le projet.
  2. Vérifiez que le nom de la branche correspond à celui spécifié dans l'atelier de programmation. Par exemple, dans la capture d'écran suivante, le nom de la branche est main.

1e4c0d2c081a8fd2.png

  1. Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une fenêtre pop-up.

1debcf330fd04c7b.png

  1. Dans la fenêtre pop-up, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
  2. Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
  3. Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.

Ouvrir le projet dans Android Studio

  1. Lancez Android Studio.
  2. Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open (Ouvrir).

d8e9dbdeafe9038a.png

Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > Open (Fichier > Ouvrir).

8d1fda7396afe8e5.png

  1. Dans l'explorateur de fichiers, accédez à l'emplacement du dossier du projet décompressé (il se trouve probablement dans le dossier Téléchargements).
  2. Double-cliquez sur le dossier de ce projet.
  3. Attendez qu'Android Studio ouvre le projet.
  4. Cliquez sur le bouton Run (Exécuter) 8de56cba7583251f.png pour compiler et exécuter l'application. Assurez-vous que tout fonctionne comme prévu.

3. Ajouter une dépendance Room

Comme pour toute bibliothèque, vous devez d'abord ajouter les dépendances nécessaires pour pouvoir utiliser Room dans l'application Bus Schedule. Vous n'aurez besoin que de deux modifications mineures, une dans chaque fichier Gradle.

  1. Dans le fichier build.gradle au niveau du projet, définissez room_version dans le bloc ext.
ext {
   kotlin_version = "1.6.20"
   nav_version = "2.4.1"
   room_version = '2.4.2'
}
  1. Dans le fichier build.gradle au niveau de l'application, à la fin de la liste des dépendances, ajoutez les suivantes.
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
  1. Synchronisez les modifications et compilez le projet pour vérifier que les dépendances ont été correctement ajoutées.

Dans les pages suivantes, vous découvrirez les composants nécessaires à l'intégration de Room dans une application : les modèles, le DAO (objet d'accès aux données), les modèles d'affichage et la classe de base de données.

4. Créer une entité

Lors de l'atelier de programmation précédent, qui portait sur les bases de données relationnelles, vous avez découvert comment les données étaient organisées en tables composées de plusieurs colonnes, chacune représentant une propriété spécifique, avec un type de données spécifique. Comme les classes en Kotlin fournissent un modèle pour chaque objet, une table d'une base de données fournit un modèle pour chaque élément, ou ligne, de cette table. Il n'est donc pas surprenant qu'une classe Kotlin puisse être utilisée pour représenter chaque table de la base de données.

Lorsque vous utilisez Room, chaque table est représentée par une classe. Dans une bibliothèque ORM (Object Relational Mapping), comme c'est le cas de Room, ces classes sont souvent appelées classes de modèle ou entités.

La base de données de l'application Bus Schedule se compose d'une seule table, intitulée "schedule" (horaires), qui contient des informations de base sur l'arrivée des bus.

  • id : nombre entier fournissant un identifiant unique qui sert de clé primaire
  • stop_name : une chaîne de caractères
  • arrival_time : un nombre entier

Notez que les types SQL utilisés dans la base de données sont en réalité INTEGER pour Int (nombre entier) et TEXT pour String (chaîne de caractères). Toutefois, lorsque vous utilisez Room, vous ne devez vous préoccuper que des types Kotlin lorsque vous définissez vos classes de modèle. Le mappage des types de données de votre classe de modèle avec ceux utilisés dans la base de données est géré automatiquement.

Lorsqu'un projet contient de nombreux fichiers, il est recommandé de les organiser dans différents packages pour mieux contrôler les accès à chaque classe. Cela permet de trouver plus facilement les classes associées. Pour créer une entité pour la table "schedule", dans le package com.example.busschedule, ajoutez un package appelé database. Au sein même de ce package, ajoutez un autre package appelé schedule, qui contiendra votre entité. Ensuite, dans le package database.schedule, créez un fichier nommé Schedule.kt et définissez une classe de données appelée Schedule.

data class Schedule(
)

Comme indiqué dans la formation sur les principes de base de SQL, les tables de données doivent disposer d'une clé primaire permettant d'identifier chaque ligne de manière unique. La première propriété que vous allez ajouter à la classe Schedule est un nombre entier qui servira d'identifiant unique. Créez une propriété et ajoutez-lui l'annotation @PrimaryKey. Cette annotation indique à Room de traiter cette propriété comme une clé primaire lorsque de nouvelles lignes sont ajoutées.

@PrimaryKey val id: Int

Ajoutez une colonne pour le nom de l'arrêt de bus. La colonne doit être de type String. Pour les nouvelles colonnes, vous devez ajouter une annotation @ColumnInfo, qui permet de spécifier leur nom. En règle générale, en SQL, les noms des colonnes doivent comporter des mots séparés par un trait de soulignement, ce qui n'est pas le cas des propriétés Kotlin, qui utilisent le lowerCamelCase. Pour cette colonne, nous voulons également interdire les valeurs nulles. Pour ce faire, vous devez lui ajouter l'annotation @NonNull.

@NonNull @ColumnInfo(name = "stop_name") val stopName: String,

Dans la base de données, les heures d'arrivée sont représentées sous la forme de nombres entiers. Il s'agit d'un code temporel Unix qui peut être converti en une date utilisable. Bien que différentes versions de SQL permettent de convertir des dates, nous allons nous contenter des fonctions de mise en forme offertes par Kotlin. Ajoutez la colonne @NonNull suivante à la classe de modèle.

@NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int

Enfin, pour que Room reconnaisse cette classe comme pouvant être utilisée pour définir des tables de base de données, vous devez ajouter une annotation à la classe elle-même. Ajoutez @Entity sur une ligne distincte, avant le nom de la classe.

Par défaut, Room utilise le nom de la classe comme nom de la table de base de données. Ainsi, le nom de la table tel que défini par la classe est maintenant "Schedule". Vous pouvez également spécifier @Entity(tableName="schedule"), mais comme les requêtes Room ne sont pas sensibles à la casse, il n'est pas nécessaire de définir explicitement un nom de table en minuscules.

La classe de l'entité doit maintenant se présenter comme suit :

@Entity
data class Schedule(
   @PrimaryKey val id: Int,
   @NonNull @ColumnInfo(name = "stop_name") val stopName: String,
   @NonNull @ColumnInfo(name = "arrival_time") val arrivalTime: Int
)

5. Définir le DAO

La classe que vous devez ajouter ensuite pour intégrer Room est le DAO. DAO signifie "Data Access Object" (objet d'accès aux données). Il s'agit d'une classe Kotlin qui permet d'accéder à des données. Plus précisément, le DAO inclut les fonctions qui permettent de lire et de manipuler des données. L'appel d'une fonction sur le DAO équivaut à exécuter une commande SQL sur la base de données. En réalité, les fonctions DAO (comme celles que vous allez définir dans cette application) spécifient souvent une commande SQL qui vous permet d'indiquer exactement ce que la fonction doit faire. Les connaissances en SQL que vous avez acquises au cours de l'atelier de programmation précédent vous seront utiles pour définir le DAO.

  1. Ajoutez une classe DAO pour l'entité Schedule. Dans le package database.schedule, créez un fichier nommé ScheduleDao.kt et définissez une interface nommée ScheduleDao. Comme pour la classe Schedule, vous devez ajouter une annotation, cette fois-ci @Dao, pour rendre l'interface utilisable avec Room.
@Dao
interface ScheduleDao {
}
  1. L'application comporte deux écrans, chacun reposant sur une requête distincte. Le premier écran affiche tous les arrêts de bus dans l'ordre chronologique, en fonction de l'heure d'arrivée. Dans ce cas d'utilisation, la requête doit simplement récupérer toutes les colonnes et inclure une clause ORDER BY (trier par) appropriée. La requête est spécifiée en tant que chaîne transmise dans une annotation @Query. Définissez une fonction getAll() qui renvoie une liste d'objets Schedule en utilisant l'annotation @Query, comme indiqué.
@Query("SELECT * FROM schedule ORDER BY arrival_time ASC")
fun getAll(): List<Schedule>
  1. Pour la deuxième requête, vous devez également sélectionner toutes les colonnes de la table "schedule". Toutefois, vous ne souhaitez obtenir que les résultats qui correspondent au nom de l'arrêt sélectionné. Vous devez donc ajouter une clause WHERE. Vous pouvez faire référence à des valeurs Kotlin dans la requête en ajoutant le signe deux-points (:) comme préfixe (par exemple, :stopName pour le paramètre de fonction). Comme précédemment, les résultats sont classés par ordre chronologie, en fonction de l'heure d'arrivée. Définissez une fonction getByStopName() qui reçoit un paramètre String appelé stopName et renvoie une List (liste) d'objets Schedule. Comme indiqué, vous devez utiliser une annotation @Query.
@Query("SELECT * FROM schedule WHERE stop_name = :stopName ORDER BY arrival_time ASC")
fun getByStopName(stopName: String): List<Schedule>

6. Définir le ViewModel

Maintenant que vous avez configuré le DAO, vous disposez techniquement de tout ce dont vous avez besoin pour commencer à accéder à la base de données à partir de vos fragments. Toutefois, même si cette approche fonctionne en théorie, elle n'est généralement pas considérée comme une bonne pratique. En effet, dans les applications plus complexes, vous disposez probablement de plusieurs écrans qui n'accèdent qu'à une partie spécifique des données. Bien que ScheduleDao soit relativement simple, vous pouvez facilement imaginer comment la situation peut devenir extrêmement complexe lorsque votre application se compose de plusieurs écrans. Par exemple, un DAO peut se présenter comme suit :

@Dao
interface ScheduleDao {

    @Query(...)
    getForScreenOne() ...

    @Query(...)
    getForScreenTwo() ...

    @Query(...)
    getForScreenThree()

}

Le code du premier écran peut accéder à getForScreenOne(), mais il n'y a aucune raison qu'il accède aux autres méthodes. Il est recommandé de séparer la partie du DAO que vous destinez à l'affichage dans une classe distincte appelée modèle de vue. Il s'agit d'un schéma architectural communément utilisé dans les applications mobiles. L'utilisation d'un modèle de vue permet d'établir une séparation claire entre le code de l'UI de votre application et son modèle de données. Ce système permet également de tester chaque partie de votre code indépendamment. Nous verrons ce point plus en détail tout au long de votre parcours de formation au développement Android.

ee2524be13171538.png

En utilisant un modèle de vue, vous pouvez utiliser la classe ViewModel à votre avantage. La classe ViewModel est utilisée pour stocker des données liées à l'UI d'une application, et elle tient également compte du cycle de vie. En d'autres termes, elle réagit aux événements de cycle de vie, comme c'est le cas des activités et des fragments. Si de tels événements (comme la rotation de l'écran) entraînent la destruction et la recréation d'une activité ou d'un fragment, il n'est pas nécessaire de recréer le ViewModel associé. Il n'est pas possible d'accéder directement à une classe DAO. Il est donc recommandé d'utiliser la sous-classe ViewModel pour séparer la responsabilité du chargement des données de votre activité ou fragment.

  1. Pour créer une classe de modèle de vue, créez un fichier nommé BusScheduleViewModel.kt dans un nouveau package appelé viewmodels. Définissez une classe pour le modèle de vue. Elle doit recevoir un seul paramètre de type ScheduleDao.
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {
  1. Comme ce modèle de vue sera utilisé sur les deux écrans, vous devrez ajouter une méthode permettant d'obtenir les horaires complets, mais aussi les horaires filtrés par nom d'arrêt. Pour ce faire, appelez les méthodes correspondantes à partir de ScheduleDao.
fun fullSchedule(): List<Schedule> = scheduleDao.getAll()

fun scheduleForStopName(name: String): List<Schedule> = scheduleDao.getByStopName(name)

Vous avez fini de définir le modèle de vue, mais si vous instanciez simplement BusScheduleViewModel directement, le système ne fonctionnera pas comme prévu. La classe ViewModel BusScheduleViewModel étant destinée à prendre en compte le cycle de vie, elle doit être instanciée par un objet pouvant réagir aux événements de cycle de vie. Si vous l'instanciez directement dans l'un de vos fragments, ce dernier devra tout gérer, y compris la mémoire, ce qui dépasse le champ d'application de votre code. Pour remédier à cela, vous pouvez créer une classe, appelée une "fabrique", qui instancie les modèles de vue pour vous.

  1. Pour créer une fabrique, sous la classe de modèle de vue, créez une classe BusScheduleViewModelFactory qui hérite de ViewModelProvider.Factory.
class BusScheduleViewModelFactory(
   private val scheduleDao: ScheduleDao
) : ViewModelProvider.Factory {
}
  1. Vous aurez juste besoin d'un peu de code récurrent pour instancier correctement un modèle de vue. Au lieu d'initialiser la classe directement, vous allez remplacer une méthode appelée create() qui renvoie un BusScheduleViewModelFactory avec une vérification des erreurs. Implémentez create() dans la classe BusScheduleViewModelFactory comme suit.
override fun <T : ViewModel> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(BusScheduleViewModel::class.java)) {
           @Suppress("UNCHECKED_CAST")
           return BusScheduleViewModel(scheduleDao) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }

Vous pouvez maintenant instancier un objet BusScheduleViewModelFactory avec BusScheduleViewModelFactory.create(). Votre modèle de vue peut ainsi tenir compte du cycle de vie sans que votre fragment n'ait besoin de le gérer directement.

7. Créer une classe de base de données et préremplir la base de données

Maintenant que vous avez défini les modèles, le DAO et un modèle de vue pour que les fragments puissent accéder au DAO, vous devez encore indiquer à Room ce qu'il faut faire avec toutes ces classes. C'est là que la classe AppDatabase entre en jeu. Une application Android qui utilise Room, comme la vôtre, sous-classe RoomDatabase et dispose de quelques responsabilités essentielles. Dans votre appli, AppDatabase doit :

  1. spécifier quelles entités sont définies dans la base de données ;
  2. fournir un accès à une seule instance de chaque classe DAO ;
  3. effectuer toute opération de configuration supplémentaire, par exemple préremplir la base de données.

Vous vous demandez peut-être pourquoi Room ne peut pas trouver toutes les entités et objets DAO à votre place. Il est tout à fait possible que votre application dispose de plusieurs bases de données, ou que dans certains scénarios, la bibliothèque ne peut pas deviner quelles sont vos intentions, en tant que développeur. La classe AppDatabase vous permet de bénéficier d'un contrôle total sur vos modèles, vos classes DAO et toute configuration de base de données que vous souhaitez effectuer.

  1. Pour ajouter une classe AppDatabase, dans le package database, créez un fichier intitulé AppDatabase.kt et définissez la classe abstraite AppDatabase qui hérite de RoomDatabase.
abstract class AppDatabase: RoomDatabase() {
}
  1. La classe de base de données permet aux autres classes d'accéder facilement aux classes DAO. Ajoutez une fonction abstraite qui renvoie un élément ScheduleDao.
abstract fun scheduleDao(): ScheduleDao
  1. Lorsque vous utilisez une classe AppDatabase, vous devez vous assurer qu'une seule instance de la base de données existe afin d'éviter les conditions de concurrence ou d'autres problèmes potentiels. L'instance est stockée dans l'objet compagnon. Vous aurez également besoin d'une méthode qui renvoie l'instance existante ou crée la base de données pour la première fois. Vous devez donc la définir dans l'objet compagnon. Ajoutez le companion object (objet compagnon) suivant juste en dessous de la fonction scheduleDao().
companion object {
}

Dans companion object, ajoutez une propriété appelée INSTANCE de type AppDatabase. La valeur étant initialement définie sur null, le type est marqué d'un ?. Elle comporte également une annotation @Volatile. Les cas d'utilisation d'une propriété volatile sont un peu complexes pour ce cours, mais nous allons tout de même l'utiliser pour votre instance AppDatabase dans le but d'éviter d'éventuels bugs.

@Volatile
private var INSTANCE: AppDatabase? = null

Sous la propriété INSTANCE, définissez une fonction pour renvoyer l'instance AppDatabase :

fun getDatabase(context: Context): AppDatabase {
    return INSTANCE ?: synchronized(this) {
        val instance = Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "app_database")
            .createFromAsset("database/bus_schedule.db")
            .build()
        INSTANCE = instance

        instance
    }
}

Dans l'implémentation de getDatabase(), utilisez l'opérateur Elvis pour renvoyer l'instance existante de la base de données (si elle existe déjà) ou créer la base de données pour la première fois dans le cas contraire. Dans cette appli, étant donné que les données sont préremplies, vous devez également appeler createFromAsset() pour charger les données existantes. Le fichier bus_schedule.db se trouve dans le package assets.database de votre projet.

  1. Tout comme les classes de modèle et de DAO, la classe de base de données nécessite une annotation afin d'ajouter des informations spécifiques. Tous les types d'entités (vous accédez au type à l'aide de ClassName::class) sont recensés dans un tableau. Un numéro de version est attribué à la base de données, que vous allez définir sur 1. Ajoutez l'annotation @Database comme suit.
@Database(entities = arrayOf(Schedule::class), version = 1)

Maintenant que vous avez créé votre classe AppDatabase, il ne vous reste plus qu'une étape pour la rendre utilisable. Vous devez fournir une sous-classe personnalisée de la classe Application et créer une propriété lazy qui contient le résultat de getDatabase().

  1. Dans le package com.example.busschedule, ajoutez un fichier intitulé BusScheduleApplication.kt et créez une classe BusScheduleApplication qui hérite de Application.
class BusScheduleApplication : Application() {
}
  1. Ajoutez une propriété de base de données de type AppDatabase. La propriété doit être "lazy" (paresseuse) et renvoyer le résultat de l'appel de getDatabase() sur votre classe AppDatabase.
class BusScheduleApplication : Application() {
   val database: AppDatabase by lazy { AppDatabase.getDatabase(this) }
  1. Enfin, pour vous assurer que la classe BusScheduleApplication est utilisée (à la place de la classe par défaut Application), vous devez apporter une légère modification au fichier manifeste. Dans AndroidMainifest.xml, définissez la propriété android:name sur com.example.busschedule.BusScheduleApplication.
<application
    android:name="com.example.busschedule.BusScheduleApplication"
    ...

Vous avez terminé la configuration du modèle de votre application. Vous pouvez maintenant utiliser les données de Room dans votre UI. Sur les pages suivantes, vous allez créer un ListAdapter pour le RecyclerView de votre application, ce qui vous permettra à la fois de présenter les horaires des bus et de donner au système la possibilité de réagir aux changements de données de manière dynamique.

8. Créer le ListAdapter

Il est temps de concrétiser tout ce travail difficile en liant le modèle à la vue. Auparavant, lorsque vous utilisiez un RecyclerView, vous deviez avoir recours à un RecyclerView.Adapter pour présenter une liste statique de données. Bien que cela fonctionne certainement pour une application telle que Bus Schedule, il n'est pas rare de gérer en temps réel les modifications apportées aux bases de données. Même si le contenu d'un seul élément change, toute la vue recycleur est actualisée. Pour la majorité des applications qui utilisent la persistance, ce ne sera pas suffisant.

Une autre méthode pour gérer les modifications de manière dynamique consiste à utiliser ListAdapter. ListAdapter utilise AsyncListDiffer pour identifier les différences entre l'ancienne liste de données et la nouvelle. Ensuite, la vue recycleur n'est modifiée qu'en fonction des différences entre les deux listes. Vous gagnez donc en performance lors du traitement des données fréquemment mises à jour, comme c'est souvent le cas dans une application qui repose sur une base de données.

f59cc2fd4d72c551.png

L'interface utilisateur étant la même sur les deux écrans, il vous suffit de créer un seul ListAdapter et de l'appliquer aux deux.

  1. Créez un fichier BusStopAdapter.kt et une classe BusStopAdapter comme indiqué. La classe étend un ListAdapter générique qui reçoit une liste d'objets Schedule et une classe BusStopViewHolder pour l'UI. Pour BusStopViewHolder, vous transmettez également un type DiffCallback que vous définirez plus loin. De son côté, la classe BusStopAdapter reçoit également un paramètre, onItemClicked(). Cette fonction permet de gérer la navigation lorsqu'un élément est sélectionné sur le premier écran. Pour le second, il vous suffit d'indiquer une fonction vide.
class BusStopAdapter(private val onItemClicked: (Schedule) -> Unit) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) {
}
  1. Comme pour un adaptateur de vue recycleur, vous avez besoin d'un conteneur pour accéder aux vues créées à partir de votre fichier de mise en page dans le code. La mise en page des cellules est déjà créée. Il suffit de créer une classe BusStopViewHolder comme indiqué et d'implémenter la fonction bind() pour définir le texte de stopNameTextView sur le nom de l'arrêt d'une part, et le texte de arrivalTimeTextView sur la date mise en forme d'autre part.
class BusStopViewHolder(private var binding: BusStopItemBinding): RecyclerView.ViewHolder(binding.root) {
    @SuppressLint("SimpleDateFormat")
    fun bind(schedule: Schedule) {
        binding.stopNameTextView.text = schedule.stopName
        binding.arrivalTimeTextView.text = SimpleDateFormat(
            "h:mm a").format(Date(schedule.arrivalTime.toLong() * 1000)
        )
    }
}
  1. Remplacez et implémentez onCreateViewHolder(), puis gonflez la mise en page et définissez onClickListener() pour appeler onItemClicked() sur l'élément à la position actuelle.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BusStopViewHolder {
   val viewHolder = BusStopViewHolder(
       BusStopItemBinding.inflate(
           LayoutInflater.from( parent.context),
           parent,
           false
       )
   )
   viewHolder.itemView.setOnClickListener {
       val position = viewHolder.adapterPosition
       onItemClicked(getItem(position))
   }
   return viewHolder
}
  1. Remplacez et implémentez onBindViewHolder(), puis liez la vue à la position spécifiée.
override fun onBindViewHolder(holder: BusStopViewHolder, position: Int) {
   holder.bind(getItem(position))
}
  1. Vous souvenez-vous de la classe DiffCallback que vous avez spécifiée pour ListAdapter ? Il s'agit simplement d'un objet qui aide ListAdapter à identifier les éléments qui ont été modifiés entre l'ancienne liste et la nouvelle après une mise à jour. Deux méthodes sont disponibles. areItemsTheSame() vérifie si l'objet (ou la ligne de la base de données, dans votre cas) est identique en ne vérifiant que l'ID. areContentsTheSame() vérifie si toutes les propriétés sont identiques, sans se limiter à l'ID. Ces méthodes permettent à ListAdapter de déterminer quels éléments ont été insérés, modifiés ou supprimés afin de refléter ces changements sur l'interface utilisateur.

Ajoutez un objet compagnon et implémentez DiffCallback comme indiqué.

companion object {
   private val DiffCallback = object : DiffUtil.ItemCallback<Schedule>() {
       override fun areItemsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
           return oldItem.id == newItem.id
       }

       override fun areContentsTheSame(oldItem: Schedule, newItem: Schedule): Boolean {
           return oldItem == newItem
       }
   }
}

La configuration de l'adaptateur est maintenant terminée. Vous allez maintenant devoir l'utiliser sur les deux écrans de l'application.

  1. Tout d'abord, dans FullScheduleFragment.kt, vous devez obtenir une référence au modèle de vue.
private val viewModel: BusScheduleViewModel by activityViewModels {
   BusScheduleViewModelFactory(
       (activity?.application as BusScheduleApplication).database.scheduleDao()
   )
}
  1. Ensuite, dans onViewCreated(), ajoutez le code suivant pour configurer la vue recycleur et attribuer son gestionnaire de mise en page.
recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
  1. Attribuez ensuite la propriété de l'adaptateur. L'action transmise utilisera stopName pour parcourir l'écran suivant, ce qui permet de filtrer la liste des arrêts de bus.
val busStopAdapter = BusStopAdapter({
   val action = FullScheduleFragmentDirections.actionFullScheduleFragmentToStopScheduleFragment(
       stopName = it.stopName
   )
   view.findNavController().navigate(action)
})
recyclerView.adapter = busStopAdapter
  1. Enfin, pour mettre à jour la liste affichée, appelez submitList() en indiquant la liste des arrêts de bus à partir du modèle de vue.
// submitList() is a call that accesses the database. To prevent the
// call from potentially locking the UI, you should use a
// coroutine scope to launch the function. Using GlobalScope is not
// best practice, and in the next step we'll see how to improve this.
GlobalScope.launch(Dispatchers.IO) {
   busStopAdapter.submitList(viewModel.fullSchedule())
}
  1. Procédez de la même manière dans StopScheduleFragment. Commencez par obtenir une référence au modèle de vue.
private val viewModel: BusScheduleViewModel by activityViewModels {
   BusScheduleViewModelFactory(
       (activity?.application as BusScheduleApplication).database.scheduleDao()
   )
}
  1. Configurez ensuite la vue recycleur dans onViewCreated(). Cette fois, il vous suffit de transmettre un bloc (fonction) vide avec {}. L'objectif est de s'assurer que rien ne se passe lorsqu'un utilisateur appuie sur les lignes de cet écran.
recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(requireContext())
val busStopAdapter = BusStopAdapter({})
recyclerView.adapter = busStopAdapter
// submitList() is a call that accesses the database. To prevent the
// call from potentially locking the UI, you should use a
// coroutine scope to launch the function. Using GlobalScope is not
// best practice, and in the next step we'll see how to improve this.
GlobalScope.launch(Dispatchers.IO) {
   busStopAdapter.submitList(viewModel.scheduleForStopName(stopName))
}
  1. Maintenant que vous avez configuré l'adaptateur, vous avez terminé d'intégrer Room à l'application Bus Schedule. Prenez quelques instants pour exécuter l'application. La liste des heures d'arrivée devrait s'afficher. Appuyez sur une ligne pour accéder à l'écran contenant des informations supplémentaires.

9. Réagir aux modifications de données à l'aide de Flow

Votre liste est configurée pour gérer efficacement les modifications de données chaque fois que la méthode submitList() est appelée, mais votre application ne peut pas encore gérer les mises à jour de façon dynamique. Pour le constater par vous-même, essayez d'ouvrir l'outil d'inspection de bases de données et d'exécuter la requête suivante pour insérer un nouvel élément dans la table "schedule".

INSERT INTO schedule
VALUES (null, 'Winding Way', 1617202500)

Vous remarquerez que l'émulateur ne réagit pas. L'utilisateur partira du principe que les données n'ont pas été modifiées. Pour que les changements s'affichent, l'application doit être relancée.

Le problème est que List<Schedule> n'est renvoyé qu'une seule fois par chacune des fonctions DAO. Même si les données sous-jacentes sont modifées, submitList() ne sera pas appelé pour mettre à jour l'UI. Par conséquent, du point de vue de l'utilisateur, rien ne se passe.

Pour résoudre ce problème, vous pouvez utiliser une fonctionnalité Kotlin appelée flux asynchrone (souvent appelée Flow, ou "flux"), qui permet au DAO d'émettre des données en continu à partir de la base de données. Si un élément est inséré, modifié ou supprimé, le résultat est renvoyé au fragment. À l'aide d'une fonction collect(), vous pouvez appeler submitList() à l'aide de la nouvelle valeur émise par le flux. Ainsi, votre ListAdapter peut mettre à jour l'UI pour prendre en compte les nouvelles données.

  1. Pour utiliser le flux dans Bus Schedule, ouvrez ScheduleDao.kt. Pour convertir les fonctions DAO afin qu'elles renvoient un Flow, il vous suffit de remplacer le type renvoyé par la fonction getAll() par Flow<List<Schedule>>.
fun getAll(): Flow<List<Schedule>>
  1. De même, modifiez la valeur renvoyée par la fonction getByStopName().
fun getByStopName(stopName: String): Flow<List<Schedule>>
  1. Les fonctions du modèle de vue qui accèdent au DAO doivent également être mises à jour. Définissez les valeurs renvoyées sur Flow<List<Schedule>> pour fullSchedule() et scheduleForStopName().
class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() {

   fun fullSchedule(): Flow<List<Schedule>> = scheduleDao.getAll()

   fun scheduleForStopName(name: String): Flow<List<Schedule>> = scheduleDao.getByStopName(name)
}
  1. Enfin, dans FullScheduleFragment.kt, busStopAdapter doit être mis à jour lorsque vous appelez collect() sur les résultats de la requête. Comme fullSchedule() est une fonction de suspension, elle doit être appelée à partir d'une coroutine. Remplacez la ligne suivante :
busStopAdapter.submitList(viewModel.fullSchedule())

Par ce code, qui utilise le flux renvoyé par fullSchedule() :

lifecycle.coroutineScope.launch {
   viewModel.fullSchedule().collect() {
       busStopAdapter.submitList(it)
   }
}
  1. Procédez de la même façon dans StopScheduleFragment, en remplaçant l'appel à scheduleForStopName() par le code ci-dessous.
lifecycle.coroutineScope.launch {
   viewModel.scheduleForStopName(stopName).collect() {
       busStopAdapter.submitList(it)
   }
}
  1. Une fois que vous avez apporté les modifications ci-dessus, vous pouvez exécuter à nouveau l'application pour vérifier que les modifications des données sont maintenant gérées en temps réel. Une fois que l'application en cours d'exécution, revenez dans l'outil d'inspection de bases de données, puis envoyez la requête suivante pour insérer une nouvelle heure d'arrivée avant 8 h 00 du matin.
INSERT INTO schedule
VALUES (null, 'Winding Way', 1617202500)

Le nouvel élément doit apparaître en haut de la liste.

79d6206fc9911fa9.png

Bravo, l'application Bus Schedule est terminée ! Vous devriez maintenant disposer de connaissances de base sur l'utilisation de Room. Dans le parcours suivant, vous allez approfondir l'utilisation de Room avec une nouvelle application exemple et apprendre à enregistrer les données créées par les utilisateurs sur un appareil.

10. Code de solution

Le code de solution de cet atelier de programmation figure dans le projet et le module ci-dessous.

  1. Accédez à la page du dépôt GitHub fournie pour le projet.
  2. Vérifiez que le nom de la branche correspond à celui spécifié dans l'atelier de programmation. Par exemple, dans la capture d'écran suivante, le nom de la branche est main.

1e4c0d2c081a8fd2.png

  1. Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une fenêtre pop-up.

1debcf330fd04c7b.png

  1. Dans la fenêtre pop-up, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
  2. Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
  3. Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.

Ouvrir le projet dans Android Studio

  1. Lancez Android Studio.
  2. Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open (Ouvrir).

d8e9dbdeafe9038a.png

Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > Open (Fichier > Ouvrir).

8d1fda7396afe8e5.png

  1. Dans l'explorateur de fichiers, accédez à l'emplacement du dossier du projet décompressé (il se trouve probablement dans le dossier Téléchargements).
  2. Double-cliquez sur le dossier de ce projet.
  3. Attendez qu'Android Studio ouvre le projet.
  4. Cliquez sur le bouton Run (Exécuter) 8de56cba7583251f.png pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.

11. Félicitations !

En résumé :

  • Les tables d'une base de données SQL sont représentées dans Room par des classes Kotlin appelées "entités".
  • Le DAO fournit des méthodes correspondant aux commandes SQL qui interagissent avec la base de données.
  • ViewModel est un composant prenant en compte le cycle de vie et qui permet de séparer les données de votre application de la vue correspondante.
  • La classe AppDatabase indique à Room quelles entités utiliser, fournit un accès au DAO et s'occupe de la configuration lors de la création de la base de données.
  • ListAdapter est un adaptateur utilisé avec RecyclerView, idéal pour gérer des listes mises à jour de façon dynamique.
  • Flow est une fonctionnalité Kotlin permettant de renvoyer un flux de données. Elle peut être utilisée avec Room pour synchroniser l'interface utilisateur avec la base de données.

En savoir plus