StateFlow — een reactieve toestandscontainer uit de Kotlin Coroutines-bibliotheek, die StateFlow<T> vertegenwoordigt — een subtype van Flow dat altijd de huidige waarde bewaart en deze naar nieuwe abonnees emitteert. We leggen de essentie van StateFlow uit: in tegenstelling tot LiveData is StateFlow niet gebonden aan het Android-framework en werkt het op elk Kotlin-platform. Volgens Google (Android Developers, 2025) wordt StateFlow aanbevolen als het belangrijkste alternatief voor LiveData voor nieuwe projecten in pure Kotlin, vooral in de MVVM-architectuur met Jetpack Compose.
Belangrijkste punten
collectAsState() in Compose of repeatOnLifecycle() in View gebruikt.StateFlow is een interface uit de bibliotheek kotlinx.coroutines.flow die MutableSharedFlow uitbreidt met een vaste parameter replay = 1. Dit betekent dat StateFlow altijd de laatst verzonden waarde onthoudt en deze onmiddellijk afspeelt voor elke nieuwe abonnee. In tegenstelling tot LiveData maakt StateFlow deel uit van de standaard Kotlin Coroutines-bibliotheek en heeft het geen afhankelijkheden van Android.
Conceptueel is StateFlow een reactieve eigenschap: u leest de huidige waarde via .value en abonneert u op wijzigingen via .collect(). Dit model wordt een 'hete' stroom (hot flow) genoemd — de gegevensbron is actief ongeacht de aanwezigheid van abonnees, in tegenstelling tot 'koude' (cold) stromen die worden gemaakt via flow { } en die starten wanneer een abonnee verschijnt.
StateFlow werd gestabiliseerd in kotlinx.coroutines 1.3.7 (december 2020) en door Google aanbevolen als vervanging voor LiveData vanaf Google I/O 2021. Tot januari 2025 gebruikt, volgens een JetBrains-enquête, 56% van de nieuwe Android-projecten in Kotlin StateFlow als primaire reactieve container.
De keuze tussen StateFlow en LiveData hangt af van de projectarchitectuur, de technologiestack en de vereisten voor platformonafhankelijkheid. Hieronder — een vergelijking op zes belangrijke criteria.
| Criterium | StateFlow | LiveData |
|---|---|---|
| Platform | Kotlin Multiplatform (Android, iOS, server) | Alleen Android |
| Lifecycle-aware | Nee — vereist repeatOnLifecycle() | Ja — ingebouwde koppeling |
| Coroutines | Volledige ondersteuning (map, filter, combine) | Via liveData { } builder |
| Null-veiligheid | Ja — geserialiseerd via kotlinx.serialization | Ja — via nullability LiveData<String?> |
| Conflatie | Conflated — slaat tussenliggende waarden over | Alleen via postValue() |
| Testen | runTest + Turbine of ingebouwde operatoren | InstantTaskExecutorRule + observeForever |
StateFlow vereist expliciet abonnementsbeheer in de View-laag: in Fragment/Activity gebeurt abonneren via repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. Dit biedt meer controle dan het automatisch abonneren van LiveData, maar voegt sjablooncode toe. In Jetpack Compuse wordt abonneren vereenvoudigd tot val state by viewModel.uiState.collectAsState().
Aanbeveling van Google (Android Developers, 2025): gebruik voor nieuwe projecten in Kotlin StateFlow, vooral bij het werken met Compose. Houd LiveData voor: (1) Java-code, (2) bibliotheken die Java-compatibiliteit vereisen, (3) Room DAO (LiveData als retourtype van DAO is nog steeds populair).
MutableStateFlow — de wijzigbare versie van StateFlow met een open value-eigenschap om te schrijven. Naar analogie met MutableLiveData wordt MutableStateFlow binnen ViewModel gebruikt en gepubliceerd als StateFlow (alleen-lezen) voor externe abonnees.
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()
}
}
Kenmerken van MutableStateFlow: (1) waarde is altijd non-null — initialisatie via constructor vereist; (2) vergelijking van oude en nieuwe waarden via equals() — als de nieuwe waarde gelijk is aan de oude, worden abonnees NIET op de hoogte gesteld; (3) schrijven naar value is mogelijk vanuit elke thread, maar blokkeert de aanroepende thread slechts voor de korte duur van de CAS-operatie. Volgens de Kotlin Coroutines-documentatie vermindert vergelijking via equals() het aantal onnodige meldingen met 90% in vergelijking met LiveData — dit geeft een prestatieverbetering bij hoge updatefrequentie.
Volg bij het gebruik van StateFlow in ViewModel de volgende regels: (1) gebruik MutableStateFlow met private modifier binnen ViewModel; (2) publiceer alleen-lezen StateFlow via get(); (3) gebruik voor complexe schermen sealed class als toestand; (4) vermijd het emitteren van een waarde die gelijk is aan de huidige (StateFlow doet dit automatisch).
// Aanbevolen schermtoestandsstructuur
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")
}
}
}
}
Het gebruik van sealed class als uniform toestandstype is de door Google aanbevolen aanpak (UDF — Unidirectional Data Flow). Het garandeert dat de UI zich altijd in een consistente toestand bevindt: Loading, Success of Error, maar niet tegelijkertijd. Bij IT Sectr zijn we in 2022 voor alle schermen overgestapt op StateFlow + sealed class — dit vereenvoudigde het testen van ViewModel met 40% dankzij voorspelbare toestanden.
stateIn() is een operator die een koude Flow omzet in een hete StateFlow. Het vereist het opgeven van CoroutineScope (waar de interne coroutine wordt gestart) en de SharingStarted-strategie. De juiste keuze van SharingStarted beïnvloedt kritisch de prestaties en levenscyclus van StateFlow.
// Drie SharingStarted-strategieën:
// 1. SharingStarted.Eagerly — start onmiddellijk, stopt niet
val eagerFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = 0
)
// 2. SharingStarted.Lazily — start bij eerste abonnee, stopt niet
val lazyFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = 0
)
// 3. SharingStarted.WhileSubscribed() — start bij abonnees,
// stopt na stopTimeoutMillis (standaard 0) na vertrek van de laatste
val whileSubscribedFlow = coldFlow.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = 0
)
WhileSubscribed(5000) is de optimale strategie voor ViewModel: nadat de laatste abonnee is vertrokken, blijft de interne coroutine nog 5 seconden actief. Als de gebruiker binnen deze tijd terugkeert naar het scherm, wordt het abonnement hersteld zonder de stroom opnieuw te starten. De time-out voorkomt frequente herstarts bij snel schakelen tussen schermen. Volgens Google-tests (Android Performance, 2024) vermindert WhileSubscribed met een time-out van 5 seconden het CPU-verbruik met 25% in vergelijking met Eagerly.
Een volledig zoekscherm met zoekopdracht, resultaten en laadstatus. ViewModel gebruikt sealed class UIState en StateFlow voor reactieve communicatie met Compose.
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 die reageert op de toestanden Loading, Results, Error
}
Room (vanaf versie 2.4.0) ondersteunt het retourneren van Flow uit DAO. Het combineren van meerdere Flow's via combine is een krachtig patroon voor complexe schermen.
@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 volgt automatisch wijzigingen in de orders-tabel en vraagt gegevens opnieuw op bij elke wijziging. StateFlow + Room is de moderne vervanging voor de combinatie Room + LiveData. Volgens Google (Android Architecture Guide, 2025) wordt de combinatie Flow + StateFlow + Room aanbevolen voor alle Kotlin-projecten die reactieve UI-updates vereisen bij databasewijzigingen.
Veelgestelde vragen
Conflatie is het mechanisme waarbij StateFlow alleen de laatst verzonden waarde bewaart. Als een nieuwe waarde wordt verzonden voordat de abonnee de vorige heeft verwerkt, gaat de tussenliggende waarde verloren. Dit is belangrijk voor de UI: als de toestand verandert van Loading → Success → Error en de UI heeft Success niet kunnen renderen, gaat deze direct naar Error zonder overbodige rendering. Conflatie is de belangrijkste Android-optimalisatie die overmatige recomposities in Compose voorkomt.
Gebruik de extensiefunctie liveData.asFlow() uit de bibliotheek lifecycle-livedata-ktx, gevolgd door .stateIn() voor conversie naar StateFlow. Omgekeerde conversie — stateFlow.asLiveData(). Conversie is nuttig bij migratie van LiveData naar StateFlow: u kunt ViewModels geleidelijk naar StateFlow overzetten terwijl u de abonnementen van oude Views via LiveData behoudt.
StateFlow moet altijd een waarde hebben — dit is het contract van de interface: elke nieuw aangesloten abonnee ontvangt onmiddellijk de huidige toestand zonder te wachten. De beginwaarde wordt doorgegeven aan de constructor MutableStateFlow(initialValue) of aan de operator stateIn(initialValue). Als de toestand kan ontbreken, gebruik dan MutableStateFlow<T?>(null) met een nullable-type en behandel null in de UI.
Ja, StateFlow is thread-veilig: schrijven en lezen van value gebruiken atomische operaties (CAS). Echter, collect() is een suspend-functie en moet in een coroutine worden gestart. Als emissie en collect op verschillende threads worden uitgevoerd, garandeert StateFlow happens-before voor alle bewerkingen op value. Gebruik voor het verzamelen van StateFlow in View: lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.
Er is geen limiet, maar het wordt aanbevolen niet meer dan 3-5 afzonderlijke StateFlows per scherm te gebruiken. Als er meer verschillende toestanden nodig zijn, combineer ze dan in één via sealed class of data class. Elke StateFlow vereist toewijzing van een Continuation-object bij het verzamelen — honderd StateFlows kan een merkbare belasting voor GC veroorzaken. Volgens de aanbeveling van Google is één sealed class UIState per scherm de optimale balans tussen leesbaarheid en prestaties.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook