Android Kotlin

Ce tutoriel explique comment créer une base de données (BDD) locale simple, sur un appareil Android, avec le langage Kotlin. La bibliothèque Anko SQLite est utilisée pour manipuler la base de données SQLite mise à disposition par le SDK Android.

L’intérêt de mettre en place une BDD dans une application est de pouvoir présenter, à l’utilisateur, des volumes de données (introduit au préalable dans la BDD) alors qu’il n’a pas de connexion internet.

Par ailleurs, il est préférable d’utiliser les fichiers de préférences dans le cas d’enregistrement d’un jeu de données peu volumineux (cf. Utiliser les fichiers de Préférences dans une application Android [AK 7]).

Le cas d’étude relatif à l’implémentation suivante concerne l’enregistrement de cours sur les technologies mobiles. Pour le moment, nous commençons par enregistrer seulement le titre du cours ainsi que sa durée.

Implémenter une classe de donnée

Tout d’abord, il s’agit d’implémenter un classe de donnée représentant un cours.

  1. Créez une nouvelle data class Kotlin, nommons la MobileCourse :
data class MobileCourse(val title: String, val time: Int)

Cette dernière permet de créer des objets de type MobileCourse ayant un titre, “ABC d’android”, et une durée en minute, 180, par exemple :

val course1 = MobileCourse("ABC D'Android", 120)

L’intérêt d’utiliser une classe de donnée est la génération automatique de fonction d’affichage tel que toString(), de copie d’objet, copy(), d’égalité, equals(), etc [1].

Définir les tables de la BDD

Dans notre cas, nous avons une seule table de cours. La table est nommée MobileCourse et possède 3 champs, c’est-à-dire 3 colonnes :

  • un identifiant, id
  • un titre, title
  • une durée, time

Il s’agit de créer un object Kotlin, nommons le MobileCourseTable, il permet de stocker dans des variables statiques le nom de la table ainsi que le nom des colonnes de la table. Cet object Kotlin est déclaré dans un fichier Tables.kt, ce dernier contiendra tous les objets représentant les tables de la BDD.

  1. Voici la déclaration de MobileCourseTable dans le fichier Tables.kt:

     object MobileCourseTable {
         val NAME = "MobileCourse"
         val ID = "_id"
         val TITLE = "title"
         val TIME = "time"
     }
    

Implémenter la classe DbHelper

Une classe de type DbHelper est une classe permettant de gérer la construction et la version de la BDD locale, sur un appareil physique. Dans le cadre d’un projet Android programmé exceptionnellement en langage Kotlin, nous allons utiliser la classe abstraite ManagedSQLiteOpenHelper disponible dans la bibliothèque Anko SQLite [2]. Cette classe abstraite permet de s’affranchir des opérations d’ouverte, d’écriture et de fermeture de la BDD. Elle utilise la classe SqliteOpenHelper du SDK Android.

  1. Importez la bibliothèque Anko SQLite :

    implementation "org.jetbrains.anko:anko-sqlite:$anko_version"
    
  2. Créez la classe DbHelper CourseDbHelper, elle hérite de ManagedSQLiteOpenHelper :

     class CourseDbHelper(ctx: Context) : ManagedSQLiteOpenHelper(ctx,
             DB_NAME, null, DB_VERSION) {
    
     }
    
  3. Créez un companion object dans CourseDbHelper, il contiendra les variables statiques :

     companion object {
         val DB_NAME = "course.db"
         val DB_VERSION = 1
     }
    

    Note : Ces variables statiques sont utilisées dans le constructeur codé en 2. Il s’agit de verrouiller le nom de la BDD ainsi que son numéro de version. Ces attributs sont constants, ils restent les mêmes tout au long de l’exécution de l’application.

    Après avoir renseigné le constructeur de ManagedSQLiteOpenHelper, il s’agit à présent d’implémenter ses fonctions membres :

    • onCreate() : se charge de créer les tables de la BDD
    • onUpgrade() : se charge de mettre à jour la BDD à la nouvelle version
  4. Implémentez la fonction onCreate() :

     override fun onCreate(db: SQLiteDatabase) {
         db.createTable(MobileCourseTable.NAME, true, MobileCourseTable.ID to INTEGER + PRIMARY_KEY,
                 MobileCourseTable.TITLE to TEXT,
                 MobileCourseTable.TIME to INTEGER
         )
     }
    

    Ici, il s’agit de créer les tables de la BDD, cela via la fonction createTable() appliquée à l’objet de type SQLiteDatabase. Elle attend, à minima, 3 paramètres :

    • le nom de la table
    • un booléen (à true car si la table n’existe pas alors la créer)
    • les colonnes de la table, au moins une, une paire avec le nom de la colonne et son type SQL
  5. Implémenter la fonction onUpgrade() :

     override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
         db.dropTable(MobileCourseTable.NAME, true)
         onCreate(db)
     }
    

Pour simplifier la mise à jour de la BDD, nous allons supprimer la table existante avec la fonction dropTable() appliquée à l’objet de type SQLiteDatabase. Elle attend 2 paramètres :

  • le nom de la table
  • un booléen (à true car si la table existe alors la supprimer)

L’idéal serait de récupérer les données de la table pour les insérer dans celle de la nouvelle version de BDD.

Pour information, voici les importations faites dans CourseDbHelper :

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import org.jetbrains.anko.db.*

Implémenter la classe de gestion de la BDD

Il s’agit d’implémenter une classe Db, plus précisément CourseDb, elle contient les requêtes SQL, faites à partir d’un DbHelper, en particulier CourseDbHelper (cf. RepositoryPattern).

  1. Créez la classe Db, CourseDb, avec un constructeur prenant en paramètre un objet de type DbHelper soit CourseDbHelper :

    class CourseDb(private val dbHelper: CourseDbHelper) { }
    
  2. Dans la classe CourseDb, créez une fonction pour récupérer les cours présents dans la BDD :

     fun requestCourse() = dbHelper.use {
         select(MobileCourseTable.NAME,
                 MobileCourseTable.TITLE, MobileCourseTable.TIME)
                 .parseList(classParser<MobileCourse>())
     }
    

    La fonction requestCourse() utilise la fonction select() pour récupérer dans la table MobileCourseTable.NAME les colonnes MobileCourseTable.TITLE et MobileCourseTable.TIME. Cette dernière renvoie un résultat nécessitant d’être parsé via parseList(), cette dernière prend en paramètre un rowParser construit de cette façon : classParser<MobileCourse>().

    Attention, le nom et le type des attributs de la classe MobileCourse doivent correspondre exactement à ce que la requête de la BDD renvoie : un titre de type champs textuel et une durée de type nombre.

  3. Dans la classe CourseDb, créez la fonction saveCourse() :

     fun saveCourse(course: MobileCourse) = dbHelper.use {
         insert(MobileCourseTable.NAME,
                 MobileCourseTable.TITLE to course.title,
                 MobileCourseTable.TIME to course.time)
     }
    

    Elle prend en paramètre un objet de type MobileCourse et l’insère dans la BDD via la fonction insert().

  4. Créez la fonction saveCourses() :

     fun saveCourses(courseList: List<MobileCourse>) {
         for (c in courseList)
             saveCourse(c)
     }
    

Elle prend en paramètre une liste de MobileCourse et insère chaque élément de la liste dans la BDD via saveCourse().

Pour information, voici les importations faites dans CourseDb :

import org.jetbrains.anko.db.classParser
import org.jetbrains.anko.db.insert
import org.jetbrains.anko.db.select

Faire les requêtes depuis la classe principale

Depuis une Activity ou un Fragment, on introduit et lit la BDD dans un thread différent de l’UIThread via doAsync. Cela afin de ne pas ralentir l’interface utilisateur.

  1. Créez en variable de classe, un objet de type CourseDb :

    val courseDb by lazy { CourseDb(CourseDbHelper(applicationContext)) }
    
  2. Déclarez la variable list comme un attribut de classe :

     var list = listof<MobileCourse>()
    
  3. Créez un cours MobileCourse et introduisez le dans la BDD :

     doAsync {
       val course1 = MobileCourse("ABC Android",120)
       courseDb.saveCourse(course1)
     }
    
  4. Lisez ce qu’il y a à présent dans la BDD :

     doAsync {
       list = courseDb.requestCourse()
     }
    
  5. Ajoutez un callBack, un appel à la fonction showList(), depuis l’uiThread dans le doAsync :

     doAsync {
       list = courseDb.requestCourse()
       uiThread {
         showList()
       }
     }
    
  6. Déclarez l’interface AnkoLogger sur votre Activity ou Fragment principal, afin de pouvoir afficher rapidement des messages dans la console Logcat (cf. Utiliser la bibliothèque Anko dans un projet Android [AK 4]).

  7. Créez la fonction showList(), elle montre dans la console Logcat le contenu de list:

     private fun showList() {
             info("NB COURS : ${list.size}")
             for (c in list)
                 info("Voici un cours ${c.title}")
         }
    

Plus loin : optimisation d’allocation mémoire

Il s’agit d’implémenter le design pattern Singleton en créant une classe Application du projet Android. ainsi que dans la classe DbHelper.

Implémenter une classe Application dans un projet Android

Cette classe est instanciée tout au long de l’exécution de l’application, elle implémente aussi le patron de conception «singleton» [9].

  1. Créez la classe App, elle hérite de la classe Application :

    class App : Application() {
    
       companion object {
           lateinit var instance: App
         }
    
         override fun onCreate() {
           super.onCreate()
           instance = this
       }
    }
    
  2. Spécifiez la nouvelle classe App dans le fichier Manifest en ajoutant l’attribut android:name à la balise <application :

     <application
             android:name=".App"
    
  3. Modifiez le constructeur de CourseDbHelper, il devient :

    class CourseDbHelper(ctx: Context = App.instance)
    

Implémentez le design pattern singleton dans le DbHelper

  1. Ajoutez dans le companion object de la classe CourseDbHelper la constante suivante :

    val instance by lazy { CourseDbHelper() }
    
  2. Modifiez le constructeur de CourseDb, il devient :

    class CourseDb(private val dbHelper: CourseDbHelper = CourseDbHelper.instance) {
    

À présent, il est possible de créez une instance de CourseDb de cette façon :

val courseDb = CourseDb()

Finalement, cet article présente l’utilisation de la bibliothèque Anko SQLite dans un projet Android. Elle permet de manipuler des données structurées et persistantes avec SQLite (données structurées : relativement volumineuses ; données persistantes : subsistantes après l’exécution d’une application). D’ailleurs, vous trouverez la démonstration dans la partie Base de Données de l’application «Kotlin pour Android» et l’implémentation correspondante est sur GitLab [8].

Cela dit, comme mentionné dans le blog [3], cette bibliothèque n’est pas idéale pour les performances. En effet, il vaut mieux utiliser l’ORM Room [7] pour un projet de grande envergure.

Références

  1. Documentation Kotlin : Data classes
  2. Documentation Anko SQLite
  3. Blog Anko
  4. Databases on Android with Anko and Kotlin by Antonio Leiva
  5. Using Anko to run background tasks by Antonio Leiva
  6. Projet Github d’Antonio
  7. Android: Room
  8. Correction sur GitLab : MR 8 db
  9. Génie logiciel : Singleton

Partagez ou réagissez sur Twitter.

Vous avez trouvé une erreur ou voulez améliorer cet article ? Editez le directement !

Comments