Publié par

Il y a 2 semaines -

Temps de lecture 7 minutes

JetPack : Créons une application Android moderne avec LiveData et Room

Introduction :

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.

Coeur 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.

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.

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.

Base de données : Room

La library 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 :

@Entity(tableName = "ski_resorts")
data class SkiResort(@PrimaryKey @field:SerializedName("ski_resort_id") val skiResortId: Int,
                     @field:SerializedName("name") val name: String = ""
					...)

Le code complet de l’entité SkiResort.

DAO : Room

Dans la DAO on retrouve une méthode qui insère une liste de station 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é.

//Add a list of ski resorts
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(skiResortList: List<SkiResort>)

//Get all the ski resorts
@Query("SELECT skiResortId, name, country, mountainRange, slopeKm, lifts, slopes FROM ski_resorts")
fun getAllSkiResorts(): LiveData<List<Resor>>

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

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.

fun requestSkiResort(
        service: SkiResortListService,
        onSuccess: (skiResorts: List&lt;SkiResort&gt;) -> Unit,
        onError: (error: String) -> Unit) {

    ...
}

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.

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.

fun getAllSkiResorts(): LiveData<List<SkiResort>> {
	requestSkiResort(skiResortListService, {
    	skiResorts ->
        ioExecutor.execute {
        	skiResortDao.insertAll(skiResorts)
        }
    }, { error ->
		//handle error properly
    })
    return skiResortDao.getAllSkiResorts()
}

Commençons par la fin, je retourne un object 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’execute sur un thread différent de l’UI.

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.

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.

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

    override fun <T : ViewModel> create(modelClass: <T>): T {
        if (modelClass.isAssignableFrom(SkiResortListViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return SkiResortListViewModel(skiResortRepo) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

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é.

object Injection{

    private fun provideSkiResortRepo(context: Context): SkiResortRepo {
        val database = SkiResortDatabase.getInstance(context)
        return SkiResortRepo(SkiResortListService.create(), database.skiResortDao(), Executors.newSingleThreadExecutor())
    }

    fun provideViewModelFactorySkiResortList(context: Context): ViewModelProvider.Factory {
        return ViewModelFactorySkiResortList(provideSkiResortRepo(context))
    }
}

 

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 = ViewModelProviders.of(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.

/**
 * Observe changes in the list of ski resort
 */
viewModelSkiResortList.skiResortList.observe(this, Observer<List<SkiResort>> {
	adapter.submitList(it)
})

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

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 grace au LiveData qui est observé dans l’activité.

Retrouvez le code LiveData, Room et Retrofit sur Github.

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.

LiveData userLiveData = ...;
LiveData userName = Transformations.map(userLiveData, user ->; {
    return user.firstName + " " + user.lastName
});

Les dernières sessions du Android Dev Summit 2018 :

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 debug la base de données.
  • présentation de Jetpack.
  • documentation de Retrofit.

Publié par

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.