AndroidKotlinTanExpress

MVVM avec Kotlin – Android Architecure Components – Dagger 2 – Retrofit – RxAndroid

By 19 novembre 2018 No Comments

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)
                }
            )
     }

     // ...
}