StateFlow — essentie, StateFlow vs LiveData in Android

Auteur: IT Sectr Gepubliceerd: 2026-02-20 Leestijd: 9 min

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

  • StateFlow — toestandshouder uit kotlinx.coroutines.flow, die altijd één actuele waarde bewaart en deze bij abonnement emitteert.
  • MutableStateFlow — wijzigbare StateFlow met mutable value property, gebruikt binnen ViewModel en gepubliceerd als StateFlow.
  • collect() — terminale Flow-operator voor abonnement op wijzigingen; voor UI wordt collectAsState() in Compose of repeatOnLifecycle() in View gebruikt.
  • StateFlow vs LiveData: StateFlow is niet afhankelijk van Lifecycle, vereist expliciet abonnementsbeheer, maar ondersteunt coroutines en multiplatform.
  • stateIn() — operator voor het omzetten van elke Flow naar StateFlow met configuratie van de SharingStarted-strategie.

Wat is StateFlow in Kotlin?

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.

StateFlow vs LiveData: belangrijkste verschillen

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.

CriteriumStateFlowLiveData
PlatformKotlin Multiplatform (Android, iOS, server)Alleen Android
Lifecycle-awareNee — vereist repeatOnLifecycle()Ja — ingebouwde koppeling
CoroutinesVolledige ondersteuning (map, filter, combine)Via liveData { } builder
Null-veiligheidJa — geserialiseerd via kotlinx.serializationJa — via nullability LiveData<String?>
ConflatieConflated — slaat tussenliggende waarden overAlleen via postValue()
TestenrunTest + Turbine of ingebouwde operatorenInstantTaskExecutorRule + 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: publicatie en abonnement

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.

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()
    }
}

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.

StateFlow in ViewModel: beste praktijken

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).

kotlin
// 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() en SharingStarted: drie strategieën

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.

kotlin
// 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.

Codevoorbeelden: StateFlow in Kotlin

Voorbeeld 1: ViewModel met StateFlow en Compose

Een volledig zoekscherm met zoekopdracht, resultaten en laadstatus. ViewModel gebruikt sealed class UIState en StateFlow voor reactieve communicatie met 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 die reageert op de toestanden Loading, Results, Error
}

Voorbeeld 2: StateFlow met Room en combine

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.

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 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

Wat is conflatie (conflation) in StateFlow?

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.

Hoe converteer ik LiveData naar StateFlow?

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.

Waarom heeft StateFlow een beginwaarde nodig?

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.

Is StateFlow thread-veilig?

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 { ... } } }.

Hoeveel StateFlows kunnen er in één ViewModel worden opgeslagen?

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

  • StateFlow — hete reactieve container uit Kotlin Coroutines (replay=1), bewaart altijd de laatste waarde.
  • StateFlow vs LiveData: StateFlow is niet afhankelijk van Lifecycle, ondersteunt coroutines en multiplatform; LiveData — automatisch abonnement.
  • MutableStateFlow met private set en publicatie van alleen-lezen StateFlow — het standaardpatroon voor ViewModel.
  • Sealed class als UIState — de door Google aanbevolen UDF-aanpak voor het beheren van complexe schermtoestanden.
  • stateIn() met WhileSubscribed(5000) — de optimale strategie voor het omzetten van koude Flow naar StateFlow voor ViewModel.
  • Room retourneert Flow uit DAO — StateFlow + combine + Room vervangt de combinatie Room + LiveData.
  • Google beveelt StateFlow aan voor nieuwe projecten in Kotlin, vooral in combinatie met Jetpack Compose.

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.

Bespreek het project

Lees ook