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.