State Hoisting est un modèle dans Jetpack Compose où l'état est déplacé d'une fonction Composable enfant vers une fonction parent, et l'enfant reçoit les données via des paramètres et notifie les changements via des callbacks. Il s'agit d'une implémentation du principe de flux de données unidirectionnel (UDF), où l'état remonte et les événements descendent. Selon Google Android Developers, 2026, le State Hoisting rend les composants réutilisables, testables et prévisibles.
Points clés
State Hoisting est un modèle où une fonction Composable ne possède pas l'état mais le reçoit de l'extérieur. Au lieu de var à l'intérieur de la fonction, deux paramètres sont utilisés : une valeur à afficher et un callback lambda pour gérer les changements. Techniquement, cela signifie que le composant enfant devient sans état (stateless), tandis que le parent est avec état (stateful).
Exemple : le composant TextField de Material3 ne stocke pas le texte saisi en interne. Il accepte value: String et onValueChange: (String) -> Unit. Le parent qui appelle TextField déclare var value by remember { mutableStateOf("") } et passe value et onValueChange. C'est du State Hoisting classique : TextField est un composant simple (affiche et signale la saisie), le parent est intelligent (possède l'état).
Stateless vs Stateful : Un composant stateless est plus facile à tester — il ne dépend pas d'un état interne, son comportement est entièrement déterminé par les paramètres d'entrée. Un composant stateful est pratique pour le prototypage rapide mais plus difficile à réutiliser : il est fortement couplé à une seule source de données. State Hoisting vous donne le choix : n'importe quel composant peut être rendu stateless en remontant l'état.
UDF (flux de données unidirectionnel) est un principe architectural où les données se déplacent dans une seule direction : de la source de vérité (ViewModel ou Composable parent) vers l'interface utilisateur, et les événements circulent dans la direction opposée. State Hoisting est l'implémentation de l'UDF au niveau des composants individuels. Au lieu que chaque composant décide quand et comment changer son propre état, il notifie le parent d'un événement, et le parent décide comment changer l'état.
Avantages de l'UDF : prévisibilité — l'état change à un seul endroit, éliminant les conditions de course ; traçabilité — la pile d'appels permet de reconstruire la chaîne de changements ; tests — la logique stateful peut être extraite dans une classe séparée et testée sans interface utilisateur. Dans les grands projets, l'UDF combiné au State Hoisting est le standard de facto.
Source unique de vérité (Single Source of Truth) est un autre principe qui accompagne l'UDF. Chaque élément d'état a exactement une source. Si deux composants utilisent le même état, la source doit être partagée (au niveau du ViewModel ou du parent commun). State Hoisting garantit que la source se trouve en haut de la hiérarchie et qu'aucune duplication d'état ne se produit.
| Direction | Ce qui est passé | Comment c'est implémenté |
|---|---|---|
| Vers le bas (parent → enfant) | Valeur à afficher | Paramètre value: T |
| Vers le haut (enfant → parent) | Événement de changement | Paramètre onValueChange: (T) -> Unit |
La règle principale : l'état doit être remonté au niveau minimum possible suffisant pour tous les composants qui en ont besoin. Si l'état n'est utilisé qu'à l'intérieur d'un seul composant — gardez-le local. Si deux composants adjacents ont besoin du même état — remontez-le au parent commun. Si l'état est nécessaire sur tout l'écran — remontez-le au ViewModel.
La règle de remontée minimale évite la complexité inutile. Il est inutile de remonter l'état d'un champ de texte à un ViewModel s'il n'est utilisé qu'à l'intérieur d'un seul écran et n'est pas persisté lors de la recréation de l'Activity. Utilisez rememberSaveable au niveau du parent de l'écran, pas dans le ViewModel, pour l'état d'interface utilisateur qui doit survivre à une rotation d'écran mais dont la logique métier n'a pas besoin.
Quand remonter au ViewModel : si l'état doit survivre à la recréation de l'Activity, s'il est nécessaire à plusieurs écrans, si le changement d'état déclenche une logique métier (requêtes réseau, base de données). State Hoisting au niveau du ViewModel est un modèle standard dans l'architecture MVVM, où la couche d'interface utilisateur est stateless et le ViewModel est stateful.
// ❌ Mauvais : le composant possède son propre état
@Composable
fun BadTextField(label: String) {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}
// ✅ Bon : State Hoisting — état dans le parent
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}
// Utilisation : le parent possède l'état
@Composable
fun Form() {
var name by rememberSaveable { mutableStateOf("") }
GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}
Considérez un écran de connexion avec deux champs (email, mot de passe) et un bouton. Les trois composants reçoivent l'état via State Hoisting : l'email et le mot de passe sont gérés par le parent, le bouton reçoit son état activé comme valeur.
// State Hoisting au niveau de l'écran
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
// Champ Email — State Hoisting via lambda
EmailField(
email = uiState.email,
onEmailChange = { viewModel.onEmailChanged(it) }
)
// Champ Mot de passe — de même
PasswordField(
password = uiState.password,
onPasswordChange = { viewModel.onPasswordChanged(it) }
)
// Bouton — reçoit seulement enabled (lecture seule)
LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
}
}
// Composant Stateless : reçoit email + callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("Email") },
singleLine = true
)
}
// Composant de bouton Stateless
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = enabled) {
Text("Connexion")
}
}
EmailField et PasswordField sont complètement stateless. Ils peuvent être réutilisés sur n'importe quel écran en les connectant à n'importe quelle source de données. LoginButton reçoit enabled en lecture seule — c'est une autre forme de State Hoisting où l'état n'est pas remonté (le bouton ne peut pas s'activer lui-même) mais est passé déjà prêt. Cette approche offre une flexibilité maximale avec un couplage minimal entre composants.
Tous les états n'ont pas besoin d'être remontés. L'état local (State à l'intérieur d'un Composable) est justifié lorsque : les données ne sont nécessaires qu'à l'intérieur d'un seul composant, elles n'affectent pas les éléments frères et ne doivent pas survivre à la recomposition d'une section spécifique. Par exemple, l'état d'animation, le focus d'un champ de saisie, la position de défilement actuelle — il est raisonnable de les garder localement.
Quand State Hoisting est nécessaire : l'état est utilisé par plusieurs composants enfants ; un changement dans un enfant doit se refléter dans un autre ; la logique des changements d'état doit être testée séparément de l'interface utilisateur ; l'état doit survivre à la recréation de l'Activity. Dans ces cas, l'état local crée des doublons et une incohérence des données.
Approche hybride : gardez l'état minimum localement, remontez le reste. La règle de Compose : « remontez l'état aussi haut que nécessaire et aussi bas que possible ». En pratique, cela signifie commencer avec remember local et ne remonter le niveau que lorsque l'accès depuis un autre composant est nécessaire. N'appliquez pas State Hoisting de manière préventive — cela complexifie le code sans nécessité.
Foire aux questions
State Hoisting est un modèle au niveau des composants d'interface utilisateur. ViewModel est une couche architecturale pour la logique métier. State Hoisting peut remonter l'état au niveau du Composable parent, au niveau de l'écran ou au ViewModel. ViewModel est le point de remontée le plus élevé pour l'état qui doit survivre à la recréation de l'Activity.
Un composant stateless se teste simplement en passant des valeurs. Appelez le Composable avec les paramètres requis et vérifiez l'affichage via ComposeTestRule. Les changements d'état sont testés au niveau du parent ou du ViewModel — séparément de l'interface utilisateur. Cela simplifie considérablement les tests : il n'est pas nécessaire de simuler la recomposition à l'intérieur du composant.
Oui, c'est une pratique courante. Si un composant a seulement besoin d'afficher des données sans pouvoir les modifier — passez State<T> (lecture seule). Le composant sera abonné aux changements mais ne pourra pas les initier. Cela renforce l'encapsulation et protège les données contre les mutations indésirables.
Pour un passage profond, utilisez CompositionLocal ou passez via les paramètres du Composable parent. Si l'état est nécessaire sur tout l'écran — extrayez-le dans un ViewModel et utilisez collectAsState(). Passer à travers 5+ niveaux est un signe d'architecture incorrecte ; reconsidérez la hiérarchie des composants.
State Hoisting peut légèrement augmenter le nombre de recompositions car un changement dans le parent peut recomposer tous les enfants. Utilisez derivedStateOf pour filtrer les changements et les keys dans LazyColumn pour des mises à jour ciblées. Dans la plupart des scénarios, le surcoût de State Hoisting est négligeable comparé au bénéfice de maintenabilité.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi