State Hoisting è un pattern in Jetpack Compose in cui lo stato viene spostato da una funzione Composable figlia a una genitore, e la figlia riceve i dati tramite parametri e notifica le modifiche tramite callback. Si tratta di un'implementazione del principio del flusso di dati unidirezionale (UDF), in cui lo stato sale e gli eventi scendono. Secondo Google Android Developers, 2026, lo State Hoisting rende i componenti riutilizzabili, testabili e prevedibili.
Punti chiave
State Hoisting è un pattern in cui una funzione Composable non possiede lo stato ma lo riceve dall'esterno. Invece di var all'interno della funzione, vengono utilizzati due parametri: un valore da visualizzare e un callback lambda per gestire le modifiche. Tecnicamente, ciò significa che il componente figlio diventa stateless (senza stato proprio), mentre il genitore è stateful (possiede lo stato).
Esempio: il componente TextField di Material3 non memorizza internamente il testo inserito. Accetta value: String e onValueChange: (String) -> Unit. Il genitore che chiama TextField dichiara var value by remember { mutableStateOf("") } e passa value e onValueChange. Questo è State Hoisting classico: TextField è un componente semplice (visualizza e riporta l'input), il genitore è intelligente (possiede lo stato).
Stateless vs Stateful: Un componente stateless è più facile da testare — non dipende dallo stato interno, il suo comportamento è interamente determinato dai parametri di input. Un componente stateful è conveniente per il prototyping rapido ma più difficile da riutilizzare: è fortemente accoppiato a un'unica fonte di dati. State Hoisting ti dà la scelta: qualsiasi componente può essere reso stateless spostando lo stato verso l'alto.
UDF (Flusso di Dati Unidirezionale) è un principio architetturale in cui i dati si muovono in una direzione: dalla fonte della verità (ViewModel o Composable genitore) all'interfaccia utente, e gli eventi fluiscono nella direzione opposta. State Hoisting è l'implementazione di UDF a livello di componenti individuali. Invece che ogni componente decida quando e come cambiare il proprio stato, notifica il genitore di un evento, e il genitore decide come cambiare lo stato.
Vantaggi di UDF: prevedibilità — lo stato cambia in un solo punto, eliminando le race condition; tracciabilità — lo stack di chiamate permette di ricostruire la catena di modifiche; test — la logica stateful può essere estratta in una classe separata e testata senza UI. Nei progetti grandi, UDF combinato con State Hoisting è lo standard de facto.
Fonte Unica di Verità (Single Source of Truth) è un altro principio che accompagna UDF. Ogni parte di stato ha esattamente una fonte. Se due componenti usano lo stesso stato, la fonte deve essere condivisa (a livello di ViewModel o genitore comune). State Hoisting garantisce che la fonte sia in alto nella gerarchia e non si verifichi duplicazione dello stato.
| Direzione | Cosa viene passato | Come viene implementato |
|---|---|---|
| Giù (genitore → figlio) | Valore da visualizzare | Parametro value: T |
| Su (figlio → genitore) | Evento di modifica | Parametro onValueChange: (T) -> Unit |
La regola principale: lo stato dovrebbe essere sollevato al livello minimo possibile sufficiente per tutti i componenti che ne hanno bisogno. Se lo stato è usato solo all'interno di un componente — mantienilo locale. Se due componenti adiacenti hanno bisogno dello stesso stato — sollevalo al genitore comune. Se lo stato è necessario sull'intero schermo — sollevalo al ViewModel.
La regola del sollevamento minimo previene la complessità non necessaria. Non ha senso sollevare lo stato di un campo di testo in un ViewModel se viene usato solo all'interno di uno schermo e non viene persistito quando l'Activity viene ricreata. Usa rememberSaveable a livello del genitore dello schermo, non nel ViewModel, per lo stato dell'interfaccia utente che deve sopravvivere a una rotazione dello schermo ma non è necessario alla logica di business.
Quando sollevare al ViewModel: se lo stato deve sopravvivere alla ricreazione dell'Activity, se è necessario a più schermate, se il cambiamento dello stato attiva logica di business (richieste di rete, database). State Hoisting a livello di ViewModel è un pattern standard nell'architettura MVVM, dove il livello dell'interfaccia utente è stateless e il ViewModel è stateful.
// ❌ Male: il componente possiede il proprio stato
@Composable
fun BadTextField(label: String) {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}
// ✅ Bene: State Hoisting — stato nel genitore
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}
// Uso: il genitore possiede lo stato
@Composable
fun Form() {
var name by rememberSaveable { mutableStateOf("") }
GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}
Considera uno schermo di login con due campi (email, password) e un pulsante. Tutti e tre i componenti ricevono lo stato tramite State Hoisting: l'email e la password sono gestiti dal genitore, il pulsante riceve il suo stato enabled come valore.
// State Hoisting a livello di schermo
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
// Campo Email — State Hoisting tramite lambda
EmailField(
email = uiState.email,
onEmailChange = { viewModel.onEmailChanged(it) }
)
// Campo Password — similmente
PasswordField(
password = uiState.password,
onPasswordChange = { viewModel.onPasswordChanged(it) }
)
// Pulsante — riceve solo enabled (sola lettura)
LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
}
}
// Componente Stateless: riceve email + callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("Email") },
singleLine = true
)
}
// Componente pulsante Stateless
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = enabled) {
Text("Accedi")
}
}
EmailField e PasswordField sono completamente stateless. Possono essere riutilizzati su qualsiasi schermo collegandoli a qualsiasi fonte di dati. LoginButton riceve enabled in sola lettura — questa è un'altra forma di State Hoisting in cui lo stato non viene sollevato (il pulsante non può abilitarsi da solo) ma viene passato già pronto. Questo approccio offre la massima flessibilità con il minimo accoppiamento tra componenti.
Non tutti gli stati devono essere sollevati. Lo stato locale (State all'interno di un Composable) è giustificato quando: i dati sono necessari solo all'interno di un componente, non influenzano elementi fratelli e non devono sopravvivere alla ricomposizione di una sezione specifica. Ad esempio, lo stato dell'animazione, il focus di un campo di input, la posizione di scorrimento corrente — è ragionevole mantenerli localmente.
Quando State Hoisting è necessario: lo stato è usato da più componenti figli; una modifica in un figlio deve riflettersi in un altro; la logica dei cambiamenti di stato deve essere testata separatamente dall'interfaccia utente; lo stato deve sopravvivere alla ricreazione dell'Activity. In questi casi, lo stato locale crea duplicazione e incoerenza dei dati.
Approccio ibrido: mantieni lo stato minimo localmente, solleva il resto. La regola di Compose: “solleva lo stato tanto in alto quanto necessario e tanto in basso quanto possibile.” In pratica, ciò significa iniziare con remember locale e sollevare il livello solo quando è necessario l'accesso da un altro componente. Non applicare State Hoisting preventivamente — complica il codice senza necessità.
Domande frequenti
State Hoisting è un pattern a livello di componente dell'interfaccia utente. ViewModel è un livello architetturale per la logica di business. State Hoisting può sollevare lo stato al livello del Composable genitore, al livello dello schermo o al ViewModel. ViewModel è il punto di sollevamento più alto per lo stato che deve sopravvivere alla ricreazione dell'Activity.
Un componente stateless viene testato semplicemente passando valori. Chiama il Composable con i parametri richiesti e verifica la visualizzazione tramite ComposeTestRule. I cambiamenti di stato vengono testati a livello del genitore o del ViewModel — separatamente dall'interfaccia utente. Questo semplifica notevolmente i test: non è necessario simulare la ricomposizione all'interno del componente.
Sì, è una pratica comune. Se un componente ha solo bisogno di visualizzare dati senza poterli modificare — passa State<T> (sola lettura). Il componente sarà iscritto alle modifiche ma non potrà iniziarle. Questo rafforza l'incapsulamento e protegge i dati da mutazioni indesiderate.
Per il passaggio profondo usa CompositionLocal o passa attraverso i parametri del Composable genitore. Se lo stato è necessario sull'intero schermo — estrailo in un ViewModel e usa collectAsState(). Passare attraverso 5+ livelli è segno di architettura errata; riconsidera la gerarchia dei componenti.
State Hoisting può aumentare leggermente il numero di ricomposizioni poiché un cambiamento nel genitore può ricomporre tutti i figli. Usa derivedStateOf per filtrare i cambiamenti e keys in LazyColumn per aggiornamenti mirati. Nella maggior parte degli scenari, l'overhead di State Hoisting è trascurabile rispetto al beneficio di manutenibilità.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche