Qu’est-ce que le pattern MVI en Kotlin ?
Le pattern MVI signifie Model – View – Intent. C’est une façon d’organiser le code d'une interface utilisateur afin qu’il soit :
- plus clair,
- plus prévisible,
- plus facile à tester,
- moins sensible aux bugs liés à l'état.
Le but principal de MVI est de gérer l’état de l’UI de manière stable et unidirectionnelle : l’état va dans un seul sens, et tout passe par un seul flux.
Les trois éléments de MVI
1. Intent
Une Intent représente une action de l’utilisateur. Exemples :
- cliquer sur un bouton,
- taper du texte,
- glisser la page,
- rafraîchir une liste.
2. Model (ou State)
Le State est une classe qui représente l’état complet de l’écran.
Exemple :
data class UserState(
val isLoading: Boolean = false,
val userName: String? = null,
val error: String? = null
)
3. View
La View (Activity, Fragment ou Compose) affiche l’état et envoie les intentions du user vers le ViewModel.
Comment fonctionne MVI ?
En MVI, tout suit un cycle clair et toujours dans le même sens :
Intent ? ViewModel ? State ? View
? L’utilisateur fait une action ? Le ViewModel la traite ? Le ViewModel produit un nouvel état ? La View réaffiche l’écran avec cet état
Tout est prévisible et il n’y a qu’une seule source de vérité pour l’état.
Exemple simple en Kotlin
1. Les intents
sealed class UserIntent {
object LoadUser : UserIntent()
data class ChangeName(val newName: String) : UserIntent()
}
2. Le state
data class UserState(
val isLoading: Boolean = false,
val name: String = "",
val error: String? = null
)
3. Le ViewModel
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow(UserState())
val state: StateFlow = _state
fun handleIntent(intent: UserIntent) {
when (intent) {
UserIntent.LoadUser -> loadUser()
is UserIntent.ChangeName -> updateName(intent.newName)
}
}
private fun loadUser() = viewModelScope.launch {
_state.update { it.copy(isLoading = true) }
try {
val user = repository.getUser()
_state.update { it.copy(isLoading = false, name = user.name) }
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = "Erreur lors du chargement") }
}
}
private fun updateName(newName: String) {
_state.update { it.copy(name = newName) }
}
}
4. La View (exemple Compose)
@Composable
fun UserScreen(viewModel: UserViewModel) {
val state by viewModel.state.collectAsState()
if (state.isLoading) {
Text("Chargement…")
} else {
Column {
Text("Nom : ${state.name}")
Button(onClick = { viewModel.handleIntent(UserIntent.LoadUser) }) {
Text("Reload")
}
}
}
}
Pourquoi utiliser MVI ?
- Un seul flux = moins de bugs.
- État centralisé = facile à comprendre.
- Prévisible = simple à tester.
- Parfait avec Kotlin Flow + Compose.
Quand MVI est-il utile ?
- Écrans complexes,
- logique métier importante,
- besoin de stabilité et de prédictibilité,
- projet Android moderne avec Compose.
Quand éviter MVI ?
- Écrans très simples (un champ texte + un bouton).
- Si le pattern est mal compris ou appliqué.
- Si trop de boilerplate est ajouté « juste pour faire MVI ».
MVI apporte une structure claire, un état unique et un flux unidirectionnel. Il s’adapte parfaitement à Kotlin, Flow et Jetpack Compose, ce qui en fait un pattern moderne et fiable pour les applications Android.