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 :
2.
3.
4.
@Entity(tableName =
"ski_resorts"
)
data
class
SkiResort(@PrimaryKey
@field
:SerializedName("ski_resort_id"
) val
skiResortId: Int
,
@field
:SerializedName("name"
) val
name: String
= ""
...)
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é.
2.
3.
4.
5.
6.
//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<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.
2.
3.
4.
5.
6.
fun
requestSkiResort(
service: SkiResortListService,
onSuccess: (skiResorts: List<SkiResort>) -> 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.
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
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 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.
2.
3.
4.
5.
6.
7.
8.
9.
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é.
2.
3.
4.
5.
6.
7.
8.
9.
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))
}
}
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.
2.
3.
4.
5.
6.
/**
* 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.
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é.
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.
2.
3.
4.
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 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.