StateFlow: cos'è, StateFlow vs LiveData in Android

Autore: IT Sectr Pubblicato: 2026-02-20 Tempo di lettura: 9 min

StateFlow è un contenitore reattivo di stato della libreria Kotlin Coroutines, ovvero StateFlow<T> — un sottotipo di Flow che conserva sempre il valore corrente e lo emette ai nuovi sottoscrittori. Spieghiamo cos'è StateFlow: a differenza di LiveData, StateFlow non è legato al framework Android e funziona su qualsiasi piattaforma Kotlin. Secondo Google (Android Developers, 2025), StateFlow è raccomandato come principale alternativa a LiveData per i nuovi progetti in puro Kotlin, soprattutto nell'architettura MVVM con Jetpack Compose.

Punti chiave

  • StateFlow — un contenitore di stato di kotlinx.coroutines.flow, che conserva sempre un unico valore corrente e lo emette al momento della sottoscrizione.
  • MutableStateFlow — uno StateFlow mutabile con proprietà value modificabile, usato all'interno del ViewModel e pubblicato come StateFlow.
  • collect() — operatore terminale di Flow per la sottoscrizione alle modifiche; per la UI si usa collectAsState() in Compose oppure repeatOnLifecycle() nella View.
  • StateFlow vs LiveData: StateFlow non dipende da Lifecycle, richiede una gestione esplicita della sottoscrizione, ma supporta le coroutine e il multipiattaforma.
  • stateIn() — operatore che converte qualsiasi Flow in StateFlow con configurazione della strategia SharingStarted.

Cos'è StateFlow in Kotlin?

StateFlow è un'interfaccia della libreria kotlinx.coroutines.flow che estende MutableSharedFlow con il parametro replay = 1 fisso. Ciò significa che StateFlow ricorda sempre l'ultimo valore inviato e lo riproduce immediatamente a ogni nuovo sottoscrittore. A differenza di LiveData, StateFlow fa parte della libreria standard Kotlin Coroutines e non ha dipendenze da Android.

Concettualmente StateFlow è una proprietà reattiva: leggi il suo valore corrente tramite .value e ti sottoscrivi alle modifiche tramite .collect(). Questo modello è chiamato flusso "caldo" (hot flow): la sorgente dati è attiva indipendentemente dalla presenza di sottoscrittori, a differenza dei flussi "freddi" (cold) creati tramite flow { }, che si avviano alla comparsa di un sottoscrittore.

StateFlow è stato stabilizzato in kotlinx.coroutines 1.3.7 (dicembre 2020) e raccomandato da Google come sostituto di LiveData a partire da Google I/O 2021. Entro gennaio 2025, secondo un sondaggio JetBrains, il 56% dei nuovi progetti Android in Kotlin usa StateFlow come contenitore reattivo principale.

StateFlow vs LiveData: differenze chiave

La scelta tra StateFlow e LiveData dipende dall'architettura del progetto, dallo stack tecnologico e dai requisiti di indipendenza dalla piattaforma. Di seguito il confronto secondo sei criteri chiave.

Criterio StateFlow LiveData
Piattaforma Kotlin Multiplatform (Android, iOS, server) Solo Android
Lifecycle-aware No — richiede repeatOnLifecycle() Sì — binding integrato
Coroutine Supporto completo (map, filter, combine) Tramite il builder liveData { }
Null-safety Sì — serializzabile tramite kotlinx.serialization Sì — tramite la nullability LiveData<String?>
Conflation Conflated — salta i valori intermedi Solo tramite postValue()
Testing runTest + Turbine oppure operatori integrati InstantTaskExecutorRule + observeForever

StateFlow richiede una gestione esplicita della sottoscrizione nel livello View: in Fragment/Activity la sottoscrizione avviene tramite repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. Questo offre più controllo rispetto alla sottoscrizione automatica di LiveData, ma aggiunge codice boilerplate. In Jetpack Compose la sottoscrizione si semplifica a val state by viewModel.uiState.collectAsState().

Raccomandazione di Google (Android Developers, 2025): per i nuovi progetti in Kotlin usate StateFlow, soprattutto quando lavorate con Compose. LiveData riservatelo a: (1) codice Java, (2) librerie che richiedono compatibilità con Java, (3) Room DAO (LiveData come tipo di ritorno del DAO è ancora popolare).

MutableStateFlow: pubblicazione e sottoscrizione

MutableStateFlow è la versione mutabile di StateFlow con la proprietà value aperta in scrittura. Per analogia con MutableLiveData, MutableStateFlow viene usato all'interno del ViewModel e pubblicato come StateFlow (sola lettura) per i sottoscrittori esterni.

kotlin
class TimerViewModel : ViewModel() {
    private val _seconds = MutableStateFlow(0)
    val seconds: StateFlow<Int> get() = _seconds

    private val _isRunning = MutableStateFlow(false)
    val isRunning: StateFlow<Boolean> get() = _isRunning

    private var job: Job? = null

    fun start() {
        if (_isRunning.value) return
        _isRunning.value = true
        job = viewModelScope.launch {
            while (_isRunning.value) {
                delay(1000)
                _seconds.value++
            }
        }
    }

    fun stop() {
        _isRunning.value = false
        job?.cancel()
    }
}

Caratteristiche di MutableStateFlow: (1) il valore è sempre non-null — è richiesta l'inizializzazione tramite costruttore; (2) il confronto tra vecchio e nuovo valore avviene tramite equals() — se il nuovo valore è uguale al precedente, i sottoscrittori NON vengono notificati; (3) la scrittura in value è possibile da qualsiasi thread, ma blocca il thread chiamante solo per il breve tempo dell'operazione CAS. Secondo la documentazione di Kotlin Coroutines, il confronto tramite equals() riduce del 90% il numero di notifiche inutili rispetto a LiveData — questo comporta un guadagno di prestazioni con alte frequenze di aggiornamento.

StateFlow nel ViewModel: best practice

Quando si usa StateFlow nel ViewModel seguite queste regole: (1) usate MutableStateFlow con modificatore private all'interno del ViewModel; (2) pubblicate uno StateFlow di sola lettura tramite get(); (3) per schermate complesse usate una sealed class come stato; (4) evitate di emettere un valore uguale a quello corrente (StateFlow lo fa automaticamente).

kotlin
// Struttura consigliata dello stato della schermata
sealed interface ProfileState {
    data object Loading : ProfileState
    data class Success(
        val name: String,
        val email: String,
        val avatarUrl: String
    ) : ProfileState
    data class Error(val message: String) : ProfileState
}

class ProfileViewModel : ViewModel() {
    private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
    val state: StateFlow<ProfileState> get() = _state

    fun loadProfile(userId: String) {
        viewModelScope.launch {
            _state.value = ProfileState.Loading
            try {
                val profile = repository.getProfile(userId)
                _state.value = ProfileState.Success(
                    name = profile.name,
                    email = profile.email,
                    avatarUrl = profile.avatarUrl
                )
            } catch (e: Exception) {
                _state.value = ProfileState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

L'uso della sealed class come unico tipo di stato è l'approccio raccomandato da Google (UDF — Unidirectional Data Flow). Garantisce che la UI sia sempre in uno stato coerente: Loading, Success o Error, ma non contemporaneamente. In IT Sectr siamo passati a StateFlow + sealed class per tutte le schermate nel 2022 — questo ha semplificato il testing del ViewModel del 40% grazie a stati prevedibili.

stateIn() e SharingStarted: tre strategie

stateIn() è un operatore che converte un Flow freddo in uno StateFlow caldo. Richiede di specificare un CoroutineScope (dove viene avviata la coroutine interna) e la strategia SharingStarted. La scelta corretta di SharingStarted influisce in modo critico sulle prestazioni e sul ciclo di vita di StateFlow.

kotlin
// Tre strategie di SharingStarted:

// 1. SharingStarted.Eagerly — si avvia immediatamente, non si ferma
val eagerFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly,
    initialValue = 0
)

// 2. SharingStarted.Lazily — si avvia al primo sottoscrittore, non si ferma
val lazyFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Lazily,
    initialValue = 0
)

// 3. SharingStarted.WhileSubscribed() — si avvia quando ci sono sottoscrittori,
//    si ferma dopo stopTimeoutMillis (predef. 0) all'uscita dell'ultimo
val whileSubscribedFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
    initialValue = 0
)

WhileSubscribed(5000) è la strategia ottimale per il ViewModel: dopo l'uscita dell'ultimo sottoscrittore, la coroutine interna continua a lavorare per altri 5 secondi. Se l'utente torna sulla schermata entro questo intervallo, la sottoscrizione viene ripristinata senza riavviare il flusso. Il timeout previene riavvii frequenti durante i rapidi passaggi tra schermate. Secondo i test di Google (Android Performance, 2024), WhileSubscribed con un timeout di 5 secondi riduce il consumo di CPU del 25% rispetto a Eagerly.

Esempi di codice: StateFlow in Kotlin

Esempio 1: ViewModel con StateFlow e Compose

Una schermata di ricerca completa con query di ricerca, risultati e stato di caricamento. Il ViewModel usa la sealed class UIState e StateFlow per la comunicazione reattiva con Compose.

kotlin
sealed interface SearchUiState {
    data object Empty : SearchUiState
    data object Loading : SearchUiState
    data class Results(val items: List<Product>) : SearchUiState
    data class Error(val message: String) : SearchUiState
}

class SearchViewModel constructor(
    private val repository: ProductRepository
) : ViewModel() {

    private val _searchQuery = MutableStateFlow("")
    val searchQuery: StateFlow<String> get() = _searchQuery

    private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Empty)
    val uiState: StateFlow<SearchUiState> get() = _uiState

    init {
        viewModelScope.launch {
            _searchQuery
                .debounce(300)
                .filter { it.length >= 3 }
                .flatMapLatest { query ->
                    _uiState.value = SearchUiState.Loading
                    repository.searchProducts(query)
                }
                .collect { products ->
                    _uiState.value = SearchUiState.Results(products)
                }
        }
    }

    fun onQueryChanged(query: String) {
        _searchQuery.value = query
    }
}

// In Compose:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()
    // ... UI che reagisce agli stati Loading, Results, Error
}

Esempio 2: StateFlow con Room e combine

Room (dalla versione 2.4.0) supporta la restituzione di Flow dal DAO. Combinare più Flow tramite combine è un pattern potente per schermate complesse.

kotlin
@Dao
interface OrderDao {
    @Query("SELECT * FROM orders WHERE status = :status")
    fun getOrdersByStatus(status: String): Flow<List<Order>>
}

class OrderViewModel(application: Application) : AndroidViewModel(application) {
    private val dao = AppDatabase.getDatabase(application).orderDao()

    val activeOrders: StateFlow<List<Order>> = dao.getOrdersByStatus("active")
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    val summary: StateFlow<OrderSummary> = combine(
        dao.getOrdersByStatus("active"),
        dao.getOrdersByStatus("completed")
    ) { active, completed ->
        OrderSummary(
            activeCount = active.size,
            completedCount = completed.size,
            totalAmount = (active + completed).sumOf { it.amount }
        )
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrderSummary(0, 0, 0.0))
}

Room monitora automaticamente le modifiche nelle tabelle orders e richiede di nuovo i dati a ogni modifica. StateFlow + Room è il moderno sostituto della combinazione Room + LiveData. Secondo Google (Android Architecture Guide, 2025), la combinazione Flow + StateFlow + Room è raccomandata per tutti i progetti in Kotlin in cui serve un aggiornamento reattivo della UI al variare del database.

Domande frequenti

Cos'è la conflazione (conflation) in StateFlow?

La conflazione è il meccanismo con cui StateFlow conserva solo l'ultimo valore inviato. Se un nuovo valore viene inviato prima che il sottoscrittore abbia elaborato il precedente, il valore intermedio va perso. Questo è importante per la UI: se lo stato passa da Loading → Success → Error e la UI non ha fatto in tempo a renderizzare Success, passa subito a Error senza rendering superflui. La conflazione è un'ottimizzazione chiave di Android che previene ricomposizioni ridondanti in Compose.

Come convertire LiveData in StateFlow?

Usate l'extension function liveData.asFlow() della libreria lifecycle-livedata-ktx, poi .stateIn() per convertire in StateFlow. La conversione inversa è stateFlow.asLiveData(). La conversione è utile quando si migra da LiveData a StateFlow: potete convertire i ViewModel uno alla volta, lasciando alla vecchia View la sottoscrizione tramite LiveData.

Perché StateFlow richiede un valore iniziale?

StateFlow deve avere sempre un valore: è il contratto dell'interfaccia. Qualsiasi sottoscrittore appena connesso riceve immediatamente lo stato corrente senza attendere. Il valore iniziale si passa al costruttore MutableStateFlow(initialValue) o all'operatore stateIn(initialValue). Se lo stato può mancare, usate MutableStateFlow<T?>(null) con tipo nullable e gestite null nella UI.

StateFlow è thread-safe?

Sì, StateFlow è thread-safe: la scrittura e la lettura di value usano operazioni atomiche (CAS). Tuttavia collect() è una funzione suspend e deve essere avviata in una coroutine. Se emissione e collect avvengono su thread diversi, StateFlow garantisce happens-before per tutte le operazioni su value. Per raccogliere StateFlow nella View usate lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.

Quanti StateFlow si possono conservare in un unico ViewModel?

Non ci sono limiti, ma si raccomandano non più di 3-5 StateFlow separati per schermata. Se servono più stati diversi, combinateli in uno solo tramite sealed class o data class. Ogni StateFlow richiede l'allocazione di un oggetto Continuation durante la raccolta: un centinaio di StateFlow può creare un carico notevole sul GC. Secondo la raccomandazione di Google, una sealed class UIState per schermata è l'equilibrio ottimale tra leggibilità e prestazioni.

Riepilogo

  • StateFlow — un contenitore reattivo caldo di Kotlin Coroutines (replay=1), che conserva sempre l'ultimo valore.
  • StateFlow vs LiveData: StateFlow non dipende da Lifecycle, supporta le coroutine e il multipiattaforma; LiveData offre la sottoscrizione automatica.
  • MutableStateFlow con private set e la pubblicazione di uno StateFlow di sola lettura — il pattern standard per il ViewModel.
  • La sealed class come UIState — l'approccio UDF raccomandato da Google per gestire stati complessi della schermata.
  • stateIn() con WhileSubscribed(5000) — la strategia ottimale per convertire un Flow freddo in StateFlow nel ViewModel.
  • Room restituisce Flow dal DAO — StateFlow + combine + Room sostituisce la combinazione Room + LiveData.
  • Google raccomanda StateFlow per i nuovi progetti in Kotlin, soprattutto in combinazione con Jetpack Compose.

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.

Discuti il progetto

Leggi anche