#Architecture MVVM avec Compose

L’architecture MVVM sous Android se traduit par l’utilisation de ViewModel pour gérer l’état de l’UI et faire le liens avec les données.

Les viewModels sont des classes gérées par le système Android, elles sont les particularités suivantes :

  • Elles sont conservĂ©es lors des changements de configuration (rotation de l’écran)
  • Elles sont dĂ©truites lorsqu’elles ne sont plus utilisĂ©es (fin de l’activitĂ© ou du fragment, etc.)
  • Elles sont utilisĂ©es pour gĂ©rer l’état de l’UI
  • Elles sont indĂ©pendantes de la couche UI
  • Les viewModels vous donne accès Ă  un coroutineScope pour gĂ©rer des tâches asynchrones dans son cycle de vie (viewModelScope)
  • Elles ne peuvent pas ĂŞtre instanciĂ©es directement, elles doivent ĂŞtre créées via un ViewModelProvider

Pour respecter la logique MVVM vos ViewModel doivent :

  • ĂŠtre indĂ©pendants de la couche UI
    • Les viewModels sont faits pour ĂŞtre observĂ©s par les composants de l’UI et non liĂ©s fortements Ă  eux.
    • Les viewModels ne doivent pas contenir de rĂ©fĂ©rences Ă  des vues (TextView, Button, etc.)
  • Proposer des fonctions pour modifier l’état de l’UI
    • La vue ne doit pas pouvoir modifier directement l’état du viewModel, elle doit passer par des fonctions dĂ©diĂ©es.

đź’ˇ Astuce

Ayez la documentation suivante sous les yeux pour vous aider durant vos implémentations : https://developer.android.com/topic/libraries/architecture/viewmodel?hl=fr

#Pub/Sub (Publisher/Subscriber)

Comment lire les données d’un viewModel dans une vue ?

#Les outils

Pour pouvoir pleinement fonctionner, les viewModels se reposent sur le pattern Pub/Sub (Publisher/Subscriber) pour notifier les composants de l’UI des changements d’état. Il existe plusieurs outils pour mettre en place ce pattern sur Android / Kotlin :

  • LiveData (Android Jetpack) -> VouĂ© Ă  disparaĂ®tre au profit de StateFlow, utile pour les applications Android classiques
  • StateFlow (Kotlin Flows) -> UtilisĂ© pour les applications modernes, permet de gĂ©rer l’état de manière rĂ©active
  • MutableState (Jetpack Compose) -> UtilisĂ© pour les applications Jetpack Compose, permet de gĂ©rer l’état de manière rĂ©active

Les 3 outils ont leur propre particularités, mais le principe reste le même : un composant s’abonne à un Publisher (le viewModel) pour être notifié des changements d’état.

#Transmettre les événements utilisateurs

Comment transmettre les événements utilisateurs à un viewModel ?

La c’est beaucoup plus simple, il suffit de créer des fonctions dans le viewModel qui seront appelées par les composants de l’UI pour modifier l’état.

🚨 Attention

Une règle d’or : Ne jamais modifier l’état d’un viewModel directement depuis un composant de l’UI.

#ViewModels

#Voici un viewModel

kotlin
// ViewModel.kt
class MyViewModel: ViewModel() {
    private val _counter = MutableStateFlow(0)
    val counter: StateFlow<Int> = _counter

    fun incrementCounter() {
        _counter.value++
    }
}

#Utilisé par MyComposable

kotlin
// View.kt
@Composable
fun MyComposable(viewModel: MyViewModel) {
    val counter by viewModel.counter.collectAsState() // Transformation d'un événement en état

    Button(onClick = { viewModel.incrementCounter() }) {
        Text("Click me $counter")
    }
}

#Comment instancier un viewModel ?

On ne doit pas instancier directement un viewModel, on doit passer par un ViewModelProvider. Cela permet au système Android de gérer le cycle de vie du viewModel correctement.

Ainsi un viewModel qui serait déjà instancié ne le serait pas à nouveau, évitant fuite mémoire et erreur d’accès. Un viewModel qui ne serait plus utilisé serait détruit.

🛑 Attention

Si vous tenter de mettre ce code et de lancer l’app, cela ne marchera pas. Il vous faudra ajouter la dépendance androidx.lifecycle:lifecycle-viewmodel-compose pour que cela fonctionne.

#Exemple d’instanciation

kotlin
// View.kt
val viewModel = viewModel<MyViewModel>()

Si vous avez besoin de passer des paramètres à votre viewModel, vous pouvez utiliser la syntaxe suivante :

kotlin
// View.kt
val viewModel = viewModel<MyViewModel>(factory = MyViewModel.provideFactory(param1))

Et la factory, qui par convention est un singleton interne au viewModel :

kotlin
// ViewModel.kt
class MyViewModel(myParamInViewModel: String) : AndroidViewModel() {
    companion object {
        fun provideFactory(myParam: String): ViewModelProvider.Factory {
            return object : ViewModelProvider.Factory {
                @Suppress("UNCHECKED_CAST")
                override fun <T : ViewModel> create(
                    modelClass: Class<T>,
                    extras: CreationExtras
                ): T {
                    // Get the Application object from extras
                    val application = checkNotNull(extras[APPLICATION_KEY])

                    return MyViewModel(
                        myParamInViewModel = myParam,
                        (application as MyApplication)
                    ) as T
                }
            }
        }
    }

    private val _counter = MutableStateFlow(0)
    val counter: StateFlow<Int> = _counter

    fun incrementCounter() {
        _counter.value++
    }
}