Tutoriel pour apprendre à créer des applications Android avec JetPack et Kotlin

Alexandre Genet

Vous avez toujours eu envie de briller en société et de créer la nouvelle killer app. Je vous propose de construire une application Android qui liste des stations de ski. J'ai utilisé les nouveautés de Jetpack : Room et LiveData. Le langage que j'ai choisi est le Kotlin bien évidemment.

Cœur du texte

« Winter is coming », et c'est le temps de penser aux vacances de ski. Je décide donc de créer une application qui me permet de choisir ma prochaine destination.

L'application affiche une liste de stations de ski depuis un JSON hébergé sur un serveur. Rien de bien compliqué.

Afin de pouvoir penser à mes prochaines vacances, dans le métro je construis l'application en mode offline first.

Mon idée est donc de télécharger la liste de stations de ski depuis le serveur, de la stocker dans une base de données locale et d'afficher dans le RecyclerView les éléments de la base de données.

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum Commentez

- Titre : Tutoriel pour apprendre à créer des applications Android avec JetPack et Kotlin

- Auteur : Alexandre Genet

- Parution : 14 avril 2020

- Licence : Copyright ® 2020 Alexandre Genet. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Choix techniques

- II. Schéma global

- III. Base de données : Room

- IV. DAO : Room

- V. Service : Retrofit

- VI. Repository

- VII. ViewModel

- VIII. Injection

- IX. Activity

- X. Conclusion

- XI. Pour aller plus loin

- XII. Remerciements

I - Choix techniques

Afin de montrer ma passion pour Android, j'ai envie de tester de nouveaux Frameworks Jetpack et je choisis donc d'utiliser Room et LiveData.

- Room va être utile pour lire et écrire dans la base de données SQLite.

- LiveData va propager les données lues dans la base de données à l'activité.

- Retrofit va interroger le serveur distant et récupérer les données pour les insérer en base de données.

II - Schéma global

Avec un schéma ma vision du projet devient plus claire :

- En jaune, les classes liées à mon UI.

- En bleu, le repo qui va être responsable des données. Il va requêter le Web Service, interroger la base de données et persister les informations.

- En vert, les classes liées aux données soit à distance (retrofit) soit en local (Room).

Un principe d'isolation est appliqué : il n'y a pas de dépendance directe entre l'activité, Retrofit et Room.

Les données vont être observées de gauche à droite via un objet LiveData.

III - Base de données : Room

La bibliothèque Room crée une couche d'abstraction par-dessus SQLite et donne un accès simplifié et robuste à une base de données SQLite.

Une entité est implémentée pour créer la table dans la base de données :

1. @Entity(tableName = "ski_resorts")

2. data class SkiResort(@PrimaryKey @field:SerializedName("ski_resort_id") val skiResortId: Int,

3. @field:SerializedName("name") val name: String = ""

4. ...)

Le code complet de l'entité SkiResort.

IV - DAO : Room

Dans la DAO, on retrouve une méthode qui insère une liste de stations de ski. Le OnConflictStrategy.REPLACE nous permet de ne pas avoir de doublons et de mettre à jour nos données.

La méthode getAllSkiResorts nous permet de récupérer toutes les stations de ski. Elle renvoie au repository un objet LiveData, qui est observé par le ViewModel. Ce dernier est observé à son tour par l'activité.

1. //Add a list of ski resorts

2. @Insert(onConflict = OnConflictStrategy.REPLACE)

3. fun insertAll(skiResortList: List<SkiResort>)

4. //Get all the ski resorts

5. @Query("SELECT skiResortId, name, country, mountainRange, slopeKm, lifts, slopes FROM ski_resorts")

6. fun getAllSkiResorts(): LiveData<List<SkiResort>>

Je peux donc stocker et lire ma liste de stations de ski.

V - Service : Retrofit

La méthode requestSkiResort appelle le service de liste de stations de ski. Une liste de stations de ski est passée dans le cas d'un succès, tandis qu'un message est propagé en cas d'erreur.

1. fun requestSkiResort(

2. service: SkiResortListService,

3. onSuccess: (skiResorts: List<SkiResort>) -> Unit,

4. onError: (error: String) -> Unit) {

5. ...

6. }

Voici l'interface qui construit l'appel au serveur.

Il s'agit d'un appel GET sur l'URL https://firebasestorage.googleapis.com/v0/b/ski-resort-be7dc.appspot.com/o/resort.json?alt=media&token=3fe8d96d-1d30-47b6-b849-4c5aec831853.

Le code complet de la classe SkiResortListService avec l'appel au serveur.

VI - Repository

Le repository détient la donnée, celle-ci peut venir de la base de données locale ou bien d'un serveur distant, le constructeur prend donc en paramètre un service Retrofit, une DAO et un ioExecutor.

class SkiResortRepo(private val skiResortListService: SkiResortListService, private val skiResortDao: SkiResortDao, private val ioExecutor: Executor)

À la création de la vue, la méthode appelée par l'ui revoie un LiveData de liste de stations de ski.

1. fun getAllSkiResorts(): LiveData<List<SkiResort>> {

2. requestSkiResort(skiResortListService, {

3. skiResorts ->

4. ioExecutor.execute {

5. skiResortDao.insertAll(skiResorts)

6. }

7. }, { error ->

8. //handle error properly

9. })

10. return skiResortDao.getAllSkiResorts()

11. }

Commençons par la fin, je retourne un objet LiveData contenant une liste de stations de ski depuis la DAO.

On demande aussi à télécharger les données des stations de ski via un service Retrofit. Suite à la récupération des données, l'insertion de ces dernières s'exécute sur un thread différent de l'UI.

VII - ViewModel

class SkiResortListViewModel(private val skiResortRepo: SkiResortRepo) : ViewModel()

Le ViewModel contient la liste des stations de ski wrappées dans un objet LiveData.

//list of all the ski resorts
val skiResortList : LiveData<List<SkiResort>> = skiResortRepo.getAllSkiResorts()

Cette liste est initialisée à partir du repo distant.

VIII - Injection

Par défaut les ViewModels ne prennent pas de paramètres dans le constructeur, je crée donc une factory pour passer le repo de station de ski.

1. class ViewModelFactorySkiResortList(private val skiResortRepo: SkiResortRepo) : ViewModelProvider.Factory {

2. override fun <T : ViewModel> create(modelClass: <T>): T {

3. if (modelClass.isAssignableFrom(SkiResortListViewModel::class.java)) {

4. @Suppress("UNCHECKED_CAST")

5. return SkiResortListViewModel(skiResortRepo) as T

6. }

7. throw IllegalArgumentException("Unknown ViewModel class")

8. }

9. }

Un singleton Injection me permet de créer le ViewModel avec le service Retrofit, la base de données et un executor. Le résultat est l'absence de dépendances sur ces derniers depuis l'activité.

1. object Injection{

2. private fun provideSkiResortRepo(context: Context): SkiResortRepo {

3. val database = SkiResortDatabase.getInstance(context)

4. return SkiResortRepo(SkiResortListService.create(), database.skiResortDao(), Executors.newSingleThreadExecutor())

5. }

6. fun provideViewModelFactorySkiResortList(context: Context): ViewModelProvider.Factory {

7. return ViewModelFactorySkiResortList(provideSkiResortRepo(context))

8. }

9. }

IX - Activity

Dans l'activité, on déclare notre ViewModel.

private lateinit var viewModelSkiResortList: SkiResortListViewModel

Puis, dans le onCreate, le ViewModel est initialisé grâce à notre factory et objet injection.

viewModelSkiResortList = ViewModelProvider(this, Injection.provideViewModelFactorySkiResortList(this)).get(SkiResortListViewModel::class.java)

L'injection de dépendance a pour objectif de créer le ViewModel avec un repository déjà créé. Le repository est instancié avec un service Retrofit et une DAO Room.

1. /**

2. * Observe changes in the list of ski resort

3. */

4. viewModelSkiResortList.skiResortList.observe(this, Observer<List<SkiResort>> {

5. adapter.submitList(it)

6. })

Chaque modification sur la liste de stations de ski observée, l'adapter est mis à jour avec cette nouvelle liste.

X - Conclusion

Et voilà, une fois téléchargé, j'affiche des stations de ski y compris quand mon téléphone n'a pas de connexion.

Quand j'affiche l'activité de mon application, les données en base de données sont immédiatement affichées et une requête au serveur est envoyée.

Quand j'obtiens une réponse, je mets à jour ma base de données, ces changements sont propagés vers la vue grâce au LiveData qui est observé dans l'activité.

Retrouvez le code LiveData, Room et Retrofit sur Github.

XI - Pour aller plus loin

Si une première requête n'a pas réussi, nous n'avons pas de stations de ski à afficher. Il faudrait embarquer une liste dans l'application et initialiser la base de données à la création de la base de données. Voici un article avec les tips Room.

Le model est partagé entre Retrofit, Room et le viewHolder. Si par exemple le format du JSON sur le serveur change, il faudra appliquer des modifications pas forcément limitées au parsing du JSON.

Pour une architecture de code plus segmentée, il faudrait donc séparer les différentes couches de notre application :

- un model pour la vue ;

- un model pour la base de données ;

- un model pour le service Retrofit.

Il est possible de transformer les objets dans LiveData via une transformation dans le repo.

1. LiveData userLiveData = ...;

2. LiveData userName = Transformations.map(userLiveData, user -> {

3. return user.firstName + " " + user.lastName

4. });

Les dernières sessions du Android Dev Summit 2018 :

- Fun with LiveData ;

- The Room in the House.

Pour finir, des liens utiles :

- documentation sur les transformations LiveData ;

- code lab paging très intéressant pour mélanger plusieurs sources de données et ajouter de la pagination pour des listes longues ;

- l'outil Stetho pour déboguer la base de données ;

- présentation de Jetpack ;

- documentation de Retrofit.

XII - Remerciements

Cet article a été publié avec l'aimable autorisation de la société Publicis Sapient Engineering (anciennement Xebia) qui est la communauté Tech de Publicis Sapient, la branche de transformation numérique du groupe Publicis.

Nous tenons à remercier Claude Leloup pour sa correction orthographique et Winjerome pour la mise au gabarit.