Cet article est une traduction et adaptation de l’article de Gahfy disponible sur le lien suivant :
Les Projets de cet article sont disponible sur github :
MVVM sans la mise en place de Room
MVVM sans la mise en place de Room, en utilisant un Fragment
Le Projet que nous allons développer
Notre projet va se baser sur le fichier tan_arrets.json. Il simule la liste des arrêts des transports en commun de la métropole nantaise.
Il va donc falloir simuler un serveur avec l’utilitaire json-server. J’explique l’installation de cet utilitaire dans cet article.
Une fois json-server installé, il suffit de lancer la commande suivante pour simuler le serveur :
json-server --watch tan_arrets.json
On peut alors tester ce WebService via : http://localhost:3000/arrets
Voici un extrait de ce fichier :
{ "arrets": [ { "codeLieu": "OTAG", "libelle": "50 Otages", "distance": null, "ligne": [ { "numLigne": "2" }, { "numLigne": "C2"}, { "numLigne": "12"}, { "numLigne": "23"} ] }, { "codeLieu": "MAI8", "libelle": "8 Mai", "distance": null, "ligne": [ { "numLigne": "2" }, { "numLigne": "3" } ] }] }
Initialisation du projet Android
Démarrons un nouveau projet android que nous allons nommer MVVMTan, avec com.transports.mvvmtan comme package name. Nous allons aussi sélectionner Kotlin.
Nous allons choisir l’API 15 comme le SDK min, et ne pas ajouter d’activité par défaut.
Les Bases
La seule chose que nous allons implémenter ici est un BaseViewModel afin de pouvoir y injecter des dépendances.
Ajoutons d’abord un package base
à la racine de l’application.
Librairie Lifecycle
Nous souhaitons que BaseViewModel étende de la classe ViewModel d’ Android Architecture components. Nous allons donc ajouter une dépendance à la librairie d’extension Lifecycle (lifecycle extension library) – incluant les librairies ViewModel et LiveData :
Dans le fichier build.gradle du projet, ajouter la ligne suivante permettant de définir la version de la librairie lifecycle à utiliser :
ext.lifecycle_version = '1.1.1'
Les versions de cette bibliothèque sont disponible sur ce lien :
https://mvnrepository.com/artifact/android.arch.lifecycle/extensions
Dans le fichier build.gradle du module, ajouter la dépendance suivante :
dependencies { //... // LiveData & ViewModel implementation "android.arch.lifecycle:extensions:$lifecycle_version" }
Après avoir synchronisé le projet dans les fichiers gradle, vous serez en mesure d’ajouter la class BaseViewModel.
BaseViewModel
Ajouter une classe kotlin BaseViewModel
dans le package base
que nous venons tout juste de créer.
Comme nous ne l’utiliserons que pour l’injection de dépendance, nous le laisserons vide pour le moment :
Model
Maintenant que nous avons créé le BaseViewModel, concentrons-nous sur le modèle.
Nous allons créer le package « model » à la racine du projet dans le but de mettre les objets suivants :
L’objet « TanArret » tel qu’il est dans le fichier JSON. Mais aussi l’objet « Ligne », dépendant de l’objet TanArret. Cet objet Ligne sera nécessaire pour effectuer la persistence des données (Room).
Fichier Json
{ "codeLieu": "OTAG", "libelle": "50 Otages", "distance": null, "ligne": [ { "numLigne": "2" }, { "numLigne": "C2" }, { "numLigne": "12" }, { "numLigne": "23" } ] }
Kotlin
Classe TanArret.kt
data class TanArret ( var codeLieu: String, var libelle: String? = null, var distance: String? = null, var ligne: List<Ligne>? = null, )
Classe Ligne.kt
class Ligne ( var numLigne: String )
Ces deux classes kotlin seront modifiées un peu plus loin lorsque sera mis en place room.
Retrofit
Maintenant que ce modèle est terminé, nous devrons récupérer une liste d’arrêt à partir de l’API JSON (le fichier json). Parce que nous allons utiliser Retrofit pour le faire, nous allons ajouter une dépendance à cette librairie. Commencez par éditer le fichier build.gradle du projet afin d’ajouter le numéro de version de la librairie Retrofit:
ext.retrofit_version = '2.4.0'
Les versions de cette librairie sont disponible sur ce lien :
https://mvnrepository.com/artifact/com.squareup.retrofit2/retrofit
Dans le fichier build.gradle du module, ajouter les dépendances suivantes :
dependencies { //... // Retrofit implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version" implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" }
Nous avons ajoutés également les adaptateurs et les convertisseurs de Retrofit, car nous utiliserons RxJava pour s’abonner aux appels de l’API et Moshi pour analyser les fichiers JSON.
Créons maintenant un package network
dans lequel nous allons placer l’interface TanApi
en charge de récupérer les arrêts
interface TanApi { @GET("/arrets") fun getTanStops(): Observable<List<TanArret>> @GET("/arrets") fun getTanStops2(): Single<List<TanArret>> }
Ici, 2 méthodes ont été créés permettant de voir la différence entre un Observable et un Single. Le traitement sera différent et détaillé un peu plus bas.
Nous en profitons pour créer un fichier Kotlin nommé Constants.kt dans un paquet nommé utils afin de définir certaines constantes. Nous allons commencer par l’URL de base.
/** The base URL of the API */ const val BASE_URL: String = "http://10.0.2.2:3000"
Attention, avec l’adresse 10.0.2.2, vous devez obligatoirement utiliser un émulateur android. Dans ce cas là, l’adresse ip 10.0.2.2 correspondra à l’adresse ip de votre machine à partir de laquelle l’émulateur a été démarré. Si vous utilisez un vrai device pour tester l’application, vous devrez alors indiquer l’adresse ip de votre machine, où json-server a été démarré.
C’est tout. Nous allons instancier Retrofit dans la prochaine partie sur Dagger 2.
Dagger 2
Maintenant, profitons de l’injection de dépendance pour laisser les services Retrofit être injectés dans le ViewModel. Tout d’abord, ajoutons le numéro de version de la librairie Dagger dans le fichier build.gradle du projet:
ext.dagger2_version = '2.19'
Les versions de cette librairie sont disponible sur ce lien :
https://mvnrepository.com/artifact/com.google.dagger/dagger
Nous allons commencer par ajouter la dépendance de la librairie Dagger 2 dans le fichier build.gradle du module. Comme Dagger 2 inclut un processeur d’annotation, nous devrons également appliquer le plugin kotlin-apt au module:
apply plugin: 'kotlin-kapt' // ... dependencies { //... // Dagger 2 implementation "com.google.dagger:dagger:$dagger2_version" kapt "com.google.dagger:dagger-compiler:$dagger2_version" compileOnly "org.glassfish:javax.annotation:$javax_annotation" }
Nous devons donc aussi ajouter le numéro de version de la librairie javax_annotation dans le fichier build.gradle du projet:
ext.javax_annotation = '3.1.1'
Les versions de cette librairie sont disponible sur ce lien :
https://mvnrepository.com/artifact/org.glassfish/javax.annotation
Injection de Retrofit
Nous devrons créer un module pour injecter l’instance Retrofit dans le ViewModel.
Nous nommerons ce module NetworkModule et nous le placerons dans un package nommé « module » qui sera placé dans un package nommé « injection » à ajouter au package racine de votre application.
Pour ne pas instancier instancier plusieurs fois les méthodes, nous allons définir un Singleton :
/** * Module which provides all required dependencies about network */ @Module // Safe here as we are dealing with a Dagger 2 module @Suppress("unused") object NetworkModule { /** * Provides the Tan service implementation. * @param retrofit the Retrofit object used to instantiate the service * @return the Tan service implementation. */ @Provides @Reusable @JvmStatic internal fun provideTanApi(retrofit: Retrofit): TanApi { return retrofit.create(TanApi::class.java) } /** * Provides the Retrofit object. * @return the Retrofit object */ @Provides @Reusable @JvmStatic internal fun provideRetrofitInterface(): Retrofit { return Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) .build() } }
TanArret MVVM
Nous allons ajouter un package « ui » a la racine de l’application et ajouterons un package « TanArret » à celui-ci afin d’ajouter des Views et ViewModels associés aux « arrêts » qu’elle contient.
ViewModel component and injection
Créons une classe TanArretListViewModel qui sera notre ViewModel. Pour l’instant, nous n’obtiendrons que les résultats de l’API, puis nous les afficherons dans la vue.
La première chose dont nous aurons besoin est une instance de classe TanApi afin d’obtenir le résultat de l’API. Cette instance sera injectée par Dagger:
class TanArretListViewModel:BaseViewModel(){ @Inject lateinit var tanApi: TanApi }
Ensuite, nous allons créer un package « component » dans le package « injection » et créer un ViewModelInjector dans celui-ci:
@Singleton @Component(modules = [(NetworkModule::class)]) interface ViewModelInjector { /** * Injects required dependencies into the specified tanArretListViewModel. * @param TanArretListViewModel TanArretListViewModel in which to inject the dependencies */ fun inject(tanArretListViewModel: TanArretListViewModel) @Component.Builder interface Builder { fun build(): ViewModelInjector fun networkModule(networkModule: NetworkModule): Builder } }
Tout ce dont nous avons besoin de faire maintenant est d’injecter les dépendances dans la classe « BaseViewModel »
abstract class BaseViewModel: ViewModel() { private val injector: ViewModelInjector = DaggerViewModelInjector .builder() .networkModule(NetworkModule) .build() init { inject() } /** * Injects the required dependencies */ private fun inject() { when (this) { is TanArretListViewModel -> injector.inject(this) } } }
RxAndroid – Récupération des données via le ViewModel
Maintenant que l’injection de TanApi est terminée, récupérons les données à partir de l’API. Nous devrons effectuer l’appel dans le thread d’arrière-plan pendant que nous voulons effectuer des actions avec le résultat sur le thread principal Android. Pour ce faire, nous allons utiliser la bibliothèque RxAndroid. Pour ajouter une dépendance à cette librairie dans notre projet, ajoutez simplement les lignes suivantes au fichier build.gradle du module:
dependencies { //... //Rx implementation "io.reactivex.rxjava2:rxjava:$rx_java" implementation "io.reactivex.rxjava2:rxandroid:$rx_android" }
Dans le fichier build.gradle du projet, ajouter les lignes suivantes permettant de définir les version des librairies à utiliser :
ext.rx_java = '2.2.3'
ext.rx_android = '2.1.0'
Les versions de ces bibliothèques sont disponible sur ce lien :
https://mvnrepository.com/artifact/io.reactivex.rxjava2/rxjava
https://mvnrepository.com/artifact/io.reactivex.rxjava2/rxandroid
Dans la classe TanArretListViewModel, il est maintenant l’heure d’écrire la méthode permettant de récupérer les résultats :
class TanArretListViewModel: BaseViewModel(){ @Inject lateinit var tanApi: TanApi private lateinit var subscription: Disposable init { loadTanArrets() } private fun loadTanArrets(){ subscription = tanApi.getTanStops() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { onRetrieveTanArretListStart() } .doOnTerminate { onRetrieveTanArretListFinish() } .subscribe ( { onRetrieveTanArretListSuccess()}, { onRetrieveTanArretListError()} ) } private fun onRetrieveTanArretListStart() { } private fun onRetrieveTanArretListFinish() { } private fun onRetrieveTanArretListSuccess() { } private fun onRetrieveTanArretListError() { } }
ViewModel onCleared()
Nous souhaitons maintenant pouvoir « disposer » de la propriété d’abonnement (subscription) lorsque ViewModel n’est plus utilisé et sera détruit.
Android ViewModel fournit la méthode onCleared () qui sera appelée lorsque cela se produira. Nous devons donc surcharger cette méthode de la manière suivante :
class TanArretListViewModel:BaseViewModel(){ // ... private lateinit var subscription: Disposable // ... override fun onCleared() { super.onCleared() subscription.dispose() } }
LiveData
Nous allons maintenant ajouter un MutableLiveData que la vue pourra observer afin de mettre à jour la visibilité de la barre de progression que nous afficherons lors de la récupération des données depuis l’API.
class TanArretListViewModel:BaseViewModel(){ // ... val loadingVisibility: MutableLiveData<Int> = MutableLiveData() // ... private fun onRetrievePostListStart(){ loadingVisibility.value = View.VISIBLE } private fun onRetrievePostListFinish(){ loadingVisibility.value = View.GONE } private fun onRetrievePostListSuccess(){ } private fun onRetrievePostListError(){ } }
Nous allons voir comment observer le loadingVisibility dans l’activité ci-dessous.
TanArretListActivity
Il est maintenant l’heure de créer la classe TanArretActivity, dans le package ui/TanArret.
Nous allons observer le loadingVisibility pour afficher et/ou cacher le progressBar
class TanArretActivity: AppCompatActivity() { private lateinit var viewModel: TanArretListViewModel override fun onCreate(savedInstanceState: Bundle?){ super.onCreate(savedInstanceState) setContentView(R.layout.activity_tan_arret) viewModel = ViewModelProviders.of(this).get(TanArretListViewModel::class.java) viewModel.loadingVisibility.observe(this, Observer { it?.let { progressBar.visibility = it } } ) } }
Layout activity_tan_arret.xml
Il est temps maintenant de créer le layout attaché à l’activité dans le package res/layout.
Nous allons afficher pour le moment seulement une progressBar qui est par défaut non visible.
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ProgressBar android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" /> </android.support.constraint.ConstraintLayout>
Manifest
Nous devons ajouter l’activité que nous venons de créer ainsi que la permission « Internet » dans le fichier manifest pour être en mesure de tester ce que l’on vient de mettre en place
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.transports.mvvmtan"> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".ui.TanArret.TanArretActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest>
Vous pouvez à ce stade démarrer l’application. Cependant, il est fort probable que vous n’ayez pas le temps de voir la progressbar, sachant que le webservice devrait répondre très rapidement. Vous pouvez alors ajouter des logs dans l’activité pour voir si le comportement est correct :
class TanArretActivity : AppCompatActivity() { // ... override fun onCreate(savedInstanceState: Bundle?) { // ... viewModel.loadingVisibility.observe(this, Observer { it?.let { progressBar.visibility = it Log.d("MVVMTAN", "PROGRESS_BAR = $it") } } ) // ... } }
Gérer les erreurs
Nous allons maintenant afficher une SnackBar en cas d’erreur, permettant à l’utilisateur de réessayer de récupérer la liste des arrêts. Comme SnackBar fait partie de la librairie support design, nous devons ajouter cette dépendance au fichier build.gradle du module.
dependencies { //... // Support Design implementation "com.android.support:design:28.0.0" }
Nous allons maintenant ajouter la resource « string » que nous allons utiliser pour afficher le message d’erreur.
Ajouter la propriété « tanarret_error » dans le fichier res/values/strings.xml
<resources> <string name="app_name">MVVMTan</string> <string name="tanarret_error">Une erreur s\'est produite durant la récupération des arrêts</string> </resources>
Ajoutons maintenant les propriétés MutableLiveData (pour le message de l’erreur) et OnClickListener (pour l’action sur l’erreur) au ViewModel :
class TanArretListViewModel : BaseViewModel() { // ... val errorMessage: MutableLiveData<Int> = MutableLiveData() val errorClickListener = View.OnClickListener { loadTanArrets() } // ... private fun onRetrieveTanArretListError(error: Throwable) { errorMessage.value = R.string.tanarret_error }
Observons maintenant les erreurs (viewModel.errorMessage) du coté de l’activité pour afficher la SnackBar
class TanArretActivity : AppCompatActivity() { private lateinit var viewModel: TanArretListViewModel private var errorSnackbar: Snackbar? = null override fun onCreate(savedInstanceState: Bundle?) { // ... viewModel.errorMessage.observe(this, Observer { errorMessage -> errorMessage?.let { errorSnackbar = Snackbar.make(findViewById(R.id.progressBar), it, Snackbar.LENGTH_INDEFINITE) errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener) errorSnackbar?.show() } }) } }
Vous pouvez maintenant redémarrer l’application tout en étant en mode avion, et vous devriez voir apparaitre la ProgressBar suivi de la snackbar d’erreur.
Récupérer la listes des arrêts
Nous allons ajouter maintenant la propriétés MutableLiveData qui va permettre de récupérer la liste des arrêts
class TanArretListViewModel : BaseViewModel() { // ... val listTanArrets: MutableLiveData<List<TanArret>> = MutableLiveData() // ... private fun loadTanArrets() { subscription = tanApi.getTanStops() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { onRetrieveTanArretListStart() } .doOnTerminate { onRetrieveTanArretListFinish() } .subscribe( {listeDesArrets -> onRetrieveTanArretListSuccess(listeDesArrets) }, { onRetrieveTanArretListError(it) } ) } private fun onRetrieveTanArretListSuccess(listeDesArrets: List<TanArret>?) { listTanArrets.value = listeDesArrets // equivalent à listTanArrets.setValue(listeDesArrets) }
Et voici comment nous allons observer la liste des Arrêts du coté de l’activité :
class TanArretActivity : AppCompatActivity() { // ... override fun onCreate(savedInstanceState: Bundle?) { viewModel.listTanArrets.observe(this, Observer {listeDesArrets -> listeDesArrets?.forEach { Log.d("MVVMTAN","Arret ${it.codeLieu}") } }) // ... } }
Vous pouvez ensuite afficher la liste des arrêts via un adapter. Je ne vais pas détailler cette partie dans cet article. Vous pouvez regarder l’article original de Gahfy, même si des méthodes plus simple existe.
Ici s’achève la mise en place de MVVM sans la partie Room.
Le projet est disponible sur le lien suivant :
https://github.com/seb44/Example-MVVM-kotlin-dagger2-retrofit-RxAndroid-without-Room
ROOM
Nous allons maintenant mettre en place la persistance des données via la librairie Room.
Dans le fichier build.gradle du projet, ajouter la ligne suivante permettant de définir la version de la librairie room à utiliser :
ext.room_version = '1.1.1'
Les versions de cette bibliothèque sont disponible sur ce lien :
https://mvnrepository.com/artifact/android.arch.persistence.room/runtime?repo=google
Dans le fichier build.gradle du module, ajouter les informations suivantes :
dependencies { //... // Room implementation "android.arch.persistence.room:runtime:$room_version" kapt "android.arch.persistence.room:compiler:$room_version" }
Entités TanArret et Ligne
Maintenant, nous allons mettre à jour la classe TanArret pour en faire une @Entity pouvant être sauvegardée dans la base de données:
@Entity(tableName = "arret_tan") data class TanArret ( @PrimaryKey @ColumnInfo(name="code_lieu") var codeLieu: String, @ColumnInfo(name="libelle") var libelle: String? = null, @ColumnInfo(name="distance") var distance: String? = null, @ColumnInfo(name="ligne") var ligne: List<Ligne>? = null, @ColumnInfo(name="latitude") var latitude: String? = null, @ColumnInfo(name="longitude") var longitude: String? = null )
Ainsi que la classe Ligne :
@Entity class Ligne ( @ColumnInfo(name="numLigne") var numLigne: String)
Dao
Nous allons maintenant ajouter une classe DAO pour nous permettre d’insérer et de récupérer des TanArret de la base de données. Pour ce faire, ajoutons une interface nommée TanArretDao dans le package « model » :
@Dao interface TanArretDao { @Query("SELECT * from tan_arret ORDER BY code_lieu ASC") fun getTanArrets(): List<TanArret> @Insert fun insertTanArret(tanArret: TanArret) @Query("DELETE FROM tan_arret") fun deleteAllTanArrets() @Update fun updateTanArret(tanArret: TanArret) @Insert fun insertTanArrets(vararg tanArret: TanArret) }
les méthodes updateTanArret et insertTanArret ne seront pas utilisé dans cet article.
Base de Données
Nous allons définir une classe TanDatabase dans le package « database » se situant lui-même dans le package « model » :
@Database(entities = arrayOf(TanArret::class), version = 1) @TypeConverters(ConvertNumLigne::class) abstract class TanDatabase : RoomDatabase() { abstract fun arretTanDao(): TanArretDao }
Comme l’on peut voir dans la classe ci-dessus, celle-ci fait référence à une classe nomée ConvertNumLigne. Ce converter est nécessaire pour convertir la liste des numéros de ligne de la classe TanArret (ligne: List<Ligne>) en format json dans la base de donnée. Voici le contenu de cette classe :
class ConvertNumLigne { internal var gson = Gson() @TypeConverter fun stringToSomeObjectList(data: String?): List<Ligne> { if (data == null) { return Collections.emptyList() } val listType = object : TypeToken<List<Ligne>>() { }.type return gson.fromJson(data, listType) } @TypeConverter fun someObjectListToString(someObjects: List<Ligne>): String { return gson.toJson(someObjects) } }
Cette classe fait appelle à la librairie converter-gson de retrofit que l’on va devoir ajouter à notre projet.
Dans le fichier build.gradle du projet, vous devez déjà avoir la version de retrofit à utiliser :
ext.retrofit_version = '2.4.0'
Dans le fichier build.gradle du module, ajouter la dépendance suivante :
dependencies { //... // retrofit // ... implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" }
Utilisation des instances dépendantes du Context dans ViewModel
Nous devons maintenant utiliser une instance TanDao dans le ViewModel afin de récupérer des TanArret de la base de données et les insérer. Pour ce faire, nous allons ajouter un argument TanArretDao au constructeur de TanArretListViewModel. Il suffit ensuite d’appeler les méthodes insertTanArret() et getTanArrets() de l’instance TanArretDao
class TanArretListViewModel(private val tanArretDao: TanArretDao) : BaseViewModel() { @Inject lateinit var tanApi: TanApi private lateinit var subscription: Disposable val loadingVisibility: MutableLiveData<Int> = MutableLiveData() val errorMessage: MutableLiveData<Int> = MutableLiveData() val errorClickListener = View.OnClickListener { loadTanArrets() } val listTanArrets: MutableLiveData<List<TanArret>> = MutableLiveData() init { loadTanArrets() } private fun loadTanArrets() { Log.d("MVVMTAN","TanArretListViewModel - Démarrage de loadTanArrets") subscription = Observable.fromCallable { // Récupération des données sauvegardées en database locale tanArretDao.getTanArrets() } .concatMap { dbTanArretList -> if(dbTanArretList.isEmpty()) // S'il n'y a pas d'arrêts dans la base locale, appelle du WS pour les récupérer tanApi.getTanStops().concatMap {apiTanArretList -> tanArretDao.insertTanArrets(*apiTanArretList.toTypedArray()) // Sauvegarde des arrêts en local Observable.just(apiTanArretList) } else Observable.just(dbTanArretList) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { onRetrieveTanArretListStart() } .doOnTerminate { onRetrieveTanArretListFinish() } .subscribe( { result -> onRetrieveTanArretListSuccess(result) }, { onRetrieveTanArretListError(it) } ) } private fun onRetrieveTanArretListStart() { loadingVisibility.value = View.VISIBLE } private fun onRetrieveTanArretListFinish() { loadingVisibility.value = View.GONE } private fun onRetrieveTanArretListSuccess(listeDesArrets: List<TanArret>?) { listTanArrets.value = listeDesArrets // equivalent à listTanArrets.setValue(listeDesArrets) } private fun onRetrieveTanArretListError(error: Throwable) { errorMessage.value = R.string.tanarret_error } override fun onCleared() { super.onCleared() subscription.dispose() } }
ViewModelProvider.Factory
Maintenant, il suffit de créer une classe ViewModelFactory dans le package « injection » pour permettre au ViewModelProvider de savoir comment instancier notre ViewModel:
class ViewModelFactory(private val activity: AppCompatActivity): ViewModelProvider.Factory{ override fun <T : ViewModel?> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(TanArretListViewModel::class.java)) { val db = Room.databaseBuilder(activity.applicationContext, TanDatabase::class.java, "tan_database").build() @Suppress("UNCHECKED_CAST") return TanArretListViewModel(db.arretTanDao()) as T } throw IllegalArgumentException("Unknown ViewModel class") } }
Ensuite, nous devons indiquer au provider d’utiliser ce factory pour instancier la classe TanArretListViewModel. Pour ce faire, mettez à jour la méthode onCreate () de TanArretActivity:
Remplacer la ligne suivante :
class TanArretActivity : AppCompatActivity() { // ... override fun onCreate(savedInstanceState: Bundle?) { // ... viewModel = ViewModelProviders.of(this).get(TanArretListViewModel::class.java) } }
par :
class TanArretActivity : AppCompatActivity() { // ... override fun onCreate(savedInstanceState: Bundle?) { // ... viewModel = ViewModelProviders.of(this, ViewModelFactory(this)).get(TanArretListViewModel::class.java) } }
A ce stade, vous pouvez de nouveau lancer l’application. A la première exécution, la liste des arrêts étant vide en locale, le WebService va alors être appelé et la liste sera sauvegarder dans la database locale.
La liste des arrêts sera récupérer dans l’activité grâce à ceci :
viewModel.listTanArrets.observe(this, Observer { listeDesArrets -> Log.d("MVVMTAN", "Il y a ${listeDesArrets?.size} arrêts") listeDesArrets?.forEach { Log.d("MVVMTAN", "Arret ${it.codeLieu} - ${it.libelle}") } })
=> Aucun affichage est effectué, vous aurez donc un écran blanc. Cependant, vous devriez voir la liste des arrêts au niveau des logs de l’application.
Exemple Utilisation Single au lieu de Observable
Ce dernier chapitre va vous montrer comment utiliser « Single » à la place de « Observable ». Nous allons aussi modifier la logique de récupération de la liste des arrêts.
La logique existante est celle-ci :
- Récupération de la liste des arrêts dans la database locale.
- Si celle-ci n’est pas vide, affichage de cette liste
- Si celle-ci est vide, appelle du WebService, sauvegarde de la liste en locale et affichage de cette liste.
La nouvelle logique de récupération est la suivante :
- Récupération de la liste des arrêts dans la database locale.
- Affichage de celle-ci et récupération de la liste des arrêts en remote via le WebService
- Sauvegarde de la « nouvelle » liste en locale et affichage de celle-ci
=> Cette nouvelle méthode présente plusieurs avantages :
- Si une liste existe déjà en locale, l’affichage sera plus rapide que d’attendre que le WS réponde.
- Faire tout de même appel au WS permet de récupérer les potentielles modifications de la liste.
- Si pas de réseau, l’application peut toujours travailler avec la liste locale. Pas de blocage donc.
Cette nouvelle logique peut très bien se faire via un Observable. J’ai choisi d’utiliser Single pour montrer une autre façon de faire.
Pour garder l’exemple d’Observable viable dans l’application, j’ai décidé de ne pas supprimer ce qui touche à Observable.
Nous allons donc ajouter une seconde méthode (getTanArrets) dans la classe TanApi. Cette méthode va retourner non plus un Observable, mais un Single :
interface TanApi { @GET("/arrets") fun getTanStops(): Observable<List<TanArret>> @GET("/arrets") fun getTanArrets(): Single<List<TanArret>> }
Nous allons ensuite modifier la méthode loadTanArrets de la classe TanArretListViewModel. (Si vous récupérer le projet, la 1ere méthode loadTanArrets a été mis en commantaire).
class TanArretListViewModel(private val tanArretDao: TanArretDao) : BaseViewModel() { // ... fun loadTanArrets() { subscription = Observable.fromCallable { // Récupération des données sauvegardées en database locale tanArretDao.getTanArrets() } .doOnNext { dbListeTanArrets -> // Nous allons maintenant récupérer la liste des arrets coté TAN au cas ou si les arrêts ont été mis à jour tanApi.getTanArrets() .doOnSuccess { apiListeTanArrets -> // Nous avons récupéré la nouvelle liste, que nous allons sauvegarder en db locale // Suppression des données de la Base tanArretDao.deleteAllTanArrets() // Insertion des nouvelles données tanArretDao.insertTanArrets(*apiListeTanArrets.toTypedArray()) // Nous avons mis à jour la liste, Informe cette mis à jour à l'UI listTanArrets.postValue(apiListeTanArrets) } .doOnError { // Informe à l'UI qu'une erreur s'est produite errorMessage.postValue(R.string.tanarret_error) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( {}, {errorMessage.postValue(R.string.tanarret_error)}) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSubscribe { onRetrieveTanArretListStart() } .doOnTerminate { onRetrieveTanArretListFinish() } .subscribe( { dbListeDesArrets -> listTanArrets.postValue(dbListeDesArrets) }, { error -> errorMessage.postValue(R.string.tanarret_error) } ) } // ... }