Flow — wat is het, cold en hot streams in Kotlin coroutines

Auteur: IT Sectr Gepubliceerd: 2026-03-17 Leestijd: 9 min

Flow — is een type asynchrone gegevensstroom uit de Kotlin Coroutines-bibliotheek, die cold-semantiek implementeert. Volgens Kotlin Documentation, 2025 stelt Flow in staat een reeks waarden te emitteren met operatoren map, filter, catch en collect. In tegenstelling tot LiveData is Flow gebouwd op coroutines en ondersteunt het backpressure.

Belangrijkste

  • Flow — koude asynchrone gegevensstroom in Kotlin Coroutines, emitteert geen waarden tot collectie
  • Cold stream — elke abonnee start zijn eigen onafhankelijke emissie vanaf het begin
  • Hot stream (SharedFlow, StateFlow) — emitteert waarden onafhankelijk van abonnees
  • Operatoren map, filter, catch, debounce, flatMapLatest transformeren de stroom zonder blokkering
  • Flow is volledig compatibel met Jetpack Compose via StateFlow en collectAsState()

Wat is Flow in Kotlin?

Flow — is een type uit het pakket kotlinx.coroutines.flow, dat een koude asynchrone gegevensstroom vertegenwoordigt. In essentie is Flow een coroutine-sequentie die waarden emitteert via de functie emit() en eindigt met succes of met een uitzondering. Het verzamelen van de stroom gebeurt via de terminale operator collect(), die een suspend-functie is.

Cold-semantiek

Cold stream betekent dat de code in de flow-builder opnieuw wordt uitgevoerd voor elke abonnee. Observable.fromIterable in RxJava gedraagt zich vergelijkbaar: een nieuwe abonnee ontvangt alle waarden vanaf het begin. In Flow is dit geïmplementeerd via de suspend-functie collect, die de coroutine blokkeert gedurende de hele gegevensverzameling.

Flow builders

Kotlin biedt verschillende manieren om Flow te maken: flow { } — basisconstructie met emit(), flowOf(vararg values) — voor een vaste set waarden, .asFlow() — extensie voor collecties en Sequence. Alle builders zijn koud — gegevens worden alleen gegenereerd bij aanroep van de terminale operator.

Cold en Hot streams

Verdeling in cold en hot streams is een kernconcept van reactief programmeren. Cold stream (Flow, Observable) start gegevensgeneratie bij abonnement. Hot stream (Channel, SharedFlow) emitteert gegevens onafhankelijk — de abonnee ontvangt alleen wat er na abonnement gebeurt, zonder het begin van de reeks.

SharedFlow — is een hete Flow die meerdere abonnees kan hebben en bij het instellen van replay laatste waarden kan herhalen. SharedFlow is geschikt voor gebeurtenissen (eenmalige meldingen). StateFlow — zijn variant met een vaste toestandswaarde, die de laatste waarde cached voor nieuwe abonnees.

ChannelFlow gebruikt Channel onder de motorkap en combineert eigenschappen van Flow en Channel. Het ondersteunt buffering en backpressure via capaciteit (capacity). ChannelFlow is nuttig bij het converteren van callback-API naar een reactieve stroom, wanneer waarden uit verschillende coroutines worden geëmitteerd.

Conversie tussen cold en hot

Voor conversie van cold Flow naar hot SharedFlow wordt de operator shareIn(scope, started, replay) gebruikt. Parameter started regelt het startmoment: SharingStarted.WhileSubscribed() — actief zolang er abonnees zijn, Lazily — start bij eerste abonnee, Eagerly — onmiddellijke start. Omgekeerde conversie — hot naar cold: StateFlow.asFlow() retourneert een koude Flow die bij collect de huidige waarde van StateFlow emitteert. Dit is handig voor testen.

Flow operatoren

Flow biedt een rijke set operatoren die als suspend-functies binnen een coroutine werken. Operatoren hebben geen toestand en retourneren een nieuwe Flow — de originele stroom blijft ongewijzigd. Dit maakt het mogelijk veilige transformatieketens te bouwen zonder bijwerkingen.

Operator map transformeert elke waarde van de stroom via een asynchrone of synchrone transformatie. filter laat alleen waarden door die aan de voorwaarde voldoen. catch vangt uitzonderingen op vóór de terminale operator en maakt herstel van de stroom mogelijk. flatMapLatest annuleert de vorige emissie bij een nieuwe waarde — analoog aan switchMap in Rx.

Operator debounce in Flow vertraagt de publicatie van een waarde met een opgegeven timeout. Als er binnen deze tijd een nieuwe waarde komt — wordt de timer gereset. In Android wordt debounce gebruikt voor zoeken: het verzoek wordt pas na een pauze van 300-400 ms verzonden, wat het aantal API-aanroepen 3-5 keer vermindert.

Terminale operatoren

Naast collect() ondersteunt Flow andere terminale operatoren: toList() verzamelt alle waarden in een lijst — handig voor tests, first() retourneert het eerste element en annuleert de stroom, single() verwacht exact één element. fold(initial) accumuleert waarden via de doorgegeven functie. Alle terminale operatoren zijn suspend-functies en moeten worden aangeroepen binnen een coroutine of een andere suspend-functie.

Flow code voorbeelden

Eerste voorbeeld — basis Flow met getallengeneratie en transformatie via de operator map:

kotlin
val numberFlow = flow {
    for (i in 1..5) {
        delay(500)
        emit(i)
    }
}

scope.launch {
    numberFlow
        .map { "Getal: $it" }
        .collect { value ->
            println(value)
        }
}

Tweede voorbeeld — stroomtransformatie met filtering en foutafhandeling via catch:

kotlin
flow {
    emit("data1")
    emit("data2")
    throw RuntimeException("network error")
}
    .catch { e ->
        emit("fallback_data")
    }
    .collect { value ->
        println(value)
    }

Derde voorbeeld — gebruik van StateFlow in ViewModel voor reactieve UI in Jetpack Compose:

kotlin
class SearchViewModel : ViewModel() {
    private val _query = MutableStateFlow("")
    val results: StateFlow<List<Result>> = _query
        .debounce(300)
        .flatMapLatest { query ->
            repository.search(query)
        }
        .catch { emit(emptyList()) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

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

StateFlow en SharedFlow

StateFlow — is een hete Flow met een enkele huidige waarde. Het cached de laatste waarde en geeft deze onmiddellijk door aan een nieuwe abonnee. StateFlow is een Observable-container voor toestand, ondersteunt equals-vergelijking — als de nieuwe waarde overeenkomt met de huidige, vindt geen emissie plaats. Jetpack Compose gebruikt StateFlow via collectAsState().

SharedFlow — is een flexibelere hete Flow zonder verplichte beginwaarde. SharedFlow wordt geconfigureerd via replay (aantal waarden voor nieuwe abonnees), extraBufferCapacity (buffer buiten replay) en onBufferOverflow (strategie bij overloop). SharedFlow is ideaal voor eenmalige gebeurtenissen: navigatie, Snackbar, analytiek.

Flow wordt in Android-architectuur door Google aanbevolen als primaire gegevensbron (Laag: Repository → UseCase → ViewModel). LiveData is inferieur aan Flow in flexibiliteit: Flow ondersteunt coroutines, operatoren, backpressure en werkt buiten de UI-laag. Migratie van LiveData naar Flow is standaardpraktijk in moderne Android-projecten.

Bij gebruik van Flow in ViewModel is de juiste typekeuze belangrijk. StateFlow is ideaal voor UI-toestand die schermrotatie moet overleven. SharedFlow is geschikt voor gebeurtenissen waar herverwerking onaanvaardbaar is — bijvoorbeeld navigatie. Flow met collect() in lifecycleScope geeft maximale controle over de uitvoeringscontext, maar vereist handmatige annulering bij het verlaten van het scherm.

Testen van Flow gebeurt via kotlinx-coroutines-test. De bibliotheek biedt TestDispatcher — virtuele tijd die het mogelijk maakt vertragingen (delay) te versnellen en de uitvoeringsvolgorde van coroutines te regelen. TestScope.runTest { } creëert een geïsoleerde omgeving voor het testen van Flow. Operator toList() wordt vaak gebruikt in tests om alle flow-waarden met timeout te verzamelen, om te controleren of de stroom de juiste gegevensreeks heeft geëmitteerd.

Flow integreert goed met Room (Android-bibliotheek voor databases): DAO-methoden kunnen Flow<List<Entity>> retourneren. Room emitteert automatisch een nieuwe waarde bij elke tabelwijziging — UI wordt bijgewerkt zonder handmatige trigger. Dit is geïmplementeerd via InvalidationTracker, die onder de motorkap Flow met callbackFlow gebruikt. Een dergelijke benadering elimineert de noodzaak voor LiveData en maakt de gegevenslaag volledig coroutine-georiënteerd. Jetpack Compose abonneert zich via collectAsState() op StateFlow en hertekent alleen die componenten waarvan de gegevens zijn gewijzigd — dit geeft prestaties die onbereikbaar zijn met LiveData-georiënteerde architecturen. DataStore (vervanger van SharedPreferences) retourneert ook Flow<Preferences>, wat reactief lezen van app-instellingen zonder handmatige updatetriggers mogelijk maakt.

Flow ondersteunt interprocescommunicatie via kotlinx-coroutines-core op JVM zonder extra bibliotheken. In serverapplicaties op Ktor kan Flow bijvoorbeeld een stroom van inkomende WebSocket-berichten vertegenwoordigen. Elk bericht wordt in de stroom geëmitteerd, doorloopt filtering en aggregatie via operatoren, en het resultaat wordt naar de client verzonden. Een dergelijke benadering vervangt reactieve bibliotheken zoals Reactor of RxJava in Kotlin-projecten.

Compatibiliteit van Flow met bestaande RxJava-code wordt verzorgd door de module kotlinx-coroutines-rx3. De extensiefunctie Flow.asObservable() converteert Flow naar Observable uit RxJava 3. Omgekeerde conversie — CompletableSource.asFlow(), Observable.asFlow(). Dit vereenvoudigt migratie van RxJava naar coroutines: het project kan stapsgewijs worden herschreven, waarbij een deel van de lagen op RxJava blijft. Bij conversie moet rekening worden gehouden met het verschil in cold/hot-semantiek: Observable kan zowel cold als hot zijn, Flow is altijd cold voor gewone Flow en hot voor SharedFlow.

Foutafhandeling en testen van Flow

Voor foutafhandeling in Flow is er een bijzonderheid: als een uitzondering optreedt in de flow-builder vóór de terminale operator, wordt deze doorgegeven aan catch. Als een uitzondering optreedt in een operator na de builder, vangt catch na deze operator deze op. retryWhen maakt herhaalde abonnering met een voorwaarde mogelijk: herhaal bij netwerkfout tot 3 keer, maar herhaal niet bij CancellationException. Flow elimineert toestandsafhankelijke fouten omdat het geen toestand opslaat — dit vereenvoudigt debuggen in vergelijking met Observable, waar Subject interne toestand opslaat.

Testen van Flow met kotlinx-coroutines-test gebruikt TestDispatcher voor simulatie van vertragingen. Turbine — populaire bibliotheek uit de community voor het testen van Flow: test { } start Flow, awaitItem() wacht op de volgende waarde, awaitComplete() wacht op voltooiing. Turbine voegt een standaard timeout toe, wat vastlopen van tests voorkomt. Voor het testen van StateFlow gebruikt u .testIn(scope) met controle van waarden in chronologische volgorde.

Veelgestelde vragen

Wat is het verschil tussen Flow en LiveData?

Flow — is een asynchrone stream met ondersteuning voor coroutines, operatoren en backpressure, werkend op elke architectuurlaag. LiveData — is een lifecycle-aware component alleen voor de UI-laag. Google beveelt Flow aan voor bedrijfslogica en repositories, LiveData — voor eenvoudige observaties in ViewModel.

Wanneer StateFlow gebruiken in plaats van SharedFlow?

StateFlow — wanneer UI-toestand moet worden opgeslagen (takenlijst, zoektekst, laadvlag) — elke Abonnee ontvangt de huidige waarde. SharedFlow — voor eenmalige gebeurtenissen (navigatie, Snackbar). StateFlow mag niet worden gebruikt voor gebeurtenissen, omdat de nieuwe waarde opnieuw kan worden verwerkt.

Hoe werkt backpressure in Flow?

In Flow is backpressure geïmplementeerd via het suspend-mechanisme: emit() pauzeert de coroutine als de collector de vorige waarde verwerkt. Kanalen (Channel) in ChannelFlow hebben een buffer met grootte capacity. Bij overloop: suspending (wachten), drop (weggooien) of conflate (vervangen door laatste).

Hoe converteer ik callback naar Flow?

Gebruik callbackFlow — Flow-builder voor callback-API. Roep binnenin registerCallback() aan met emit(value) in de callback. awaitClose garandeert aanroep van unregisterCallback() bij annulering van de coroutine. callbackFlow ondersteunt buffering via Channel(UNLIMITED) onder de motorkap.

Kan Flow met RxJava worden gebruikt?

Ja, via converters: Flow.asObservable() uit het pakket kotlinx-coroutines-rx3 converteert Flow naar Observable uit RxJava 3. Omgekeerd — CompletableSource.asFlow() voor Single/Completable/Maybe. Dit is nuttig bij migratie van RxJava naar coroutines in grote projecten.

Samenvatting

  • Flow — koude asynchrone gegevensstroom in Kotlin Coroutines met suspend-functie collect
  • Cold stream start emissie opnieuw voor elke abonnee
  • StateFlow — hete toestandscontainer met caching van laatste waarde
  • SharedFlow — hete stream voor gebeurtenissen met replay- en bufferconfiguratie
  • Operatoren map, filter, debounce, catch, flatMapLatest — basis van stroomtransformatie
  • Google beveelt Flow aan als primaire gegevensbron in moderne Android-architectuur
  • LiveData is alleen geschikt voor de UI-laag, Flow — voor alle lagen van de app

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