Flow utilities¶
Operators, builders, and StateFlow helpers from the io.github.yuroyami.kitecore.flow package. Everything on this page is common code: it runs on every KiteCore target and depends only on kotlinx.coroutines. Each snippet is copy-pasteable and compiles against the real API.
import io.github.yuroyami.kitecore.flow.*
import kotlinx.coroutines.flow.flowOf
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
Rate limiting¶
Ignore rapid repeats with throttleFirst¶
buttonClicks
.throttleFirst(500.milliseconds)
.collect { submitOrder() }
// The first click emits immediately. Clicks arriving within 500 ms of an
// emitted click are dropped. The window reopens 500 ms after each emission.
Dropped values are lost, not conflated: after the window reopens, the next value to arrive is emitted, not the last one that was dropped. Cancelling the collector cancels the upstream and the internal timer.
Sample the newest value with throttleLatest¶
scrollOffsets
.throttleLatest(100.milliseconds)
.collect { renderMinimap(it) }
// The first offset emits immediately. While the 100 ms window is open,
// newer offsets replace each other; only the latest one emits when it elapses.
throttleFirst keeps the oldest value in each window, throttleLatest keeps the newest. Use throttleFirst for actions (button taps), throttleLatest for state you render (scroll positions, progress).
Windowing and batching¶
Sliding windows with windowed¶
flowOf(1, 2, 3, 4, 5)
.windowed(size = 3, step = 1)
.collect { println(it) }
// [1, 2, 3]
// [2, 3, 4]
// [3, 4, 5]
A window is emitted only when it is full; a trailing partial window is dropped. When step is greater than size, the elements between windows are skipped:
flowOf(1, 2, 3, 4, 5, 6, 7, 8)
.windowed(size = 2, step = 3)
.collect { println(it) }
// [1, 2]
// [4, 5]
// [7, 8]
Fixed-size chunks¶
flowOf(1, 2, 3, 4, 5)
.chunked(2)
.collect { println(it) }
// [1, 2]
// [3, 4]
// [5] <- the final partial chunk is emitted on completion
Time-bounded chunks¶
telemetryEvents
.chunked(window = 500.milliseconds, maxSize = 100)
.collect { batch -> upload(batch) }
// A chunk opens when its first event arrives and is emitted 500 ms later,
// or as soon as it holds 100 events, whichever happens first.
Empty lists are never emitted, and a non-empty chunk in progress is emitted when the upstream completes. The upstream is collected without conflation, so values wait for a slow collector instead of being lost. The two chunked overloads are distinguished by argument type: chunked(100) batches by count, chunked(500.milliseconds) batches by time.
Pairing¶
Track the previous value with withPrevious¶
flowOf("a", "b", "c")
.withPrevious()
.collect { (previous, current) -> println("$previous -> $current") }
// null -> a
// a -> b
// b -> c
The first pair carries null as the previous value, so every upstream value reaches the collector.
Consecutive pairs with pairwise¶
The transform overload computes a value from each pair directly:
temperatureReadings
.pairwise { previous, current -> current - previous }
.collect { delta -> println("changed by $delta") }
// Emits the difference between each reading and the one before it.
Flows with fewer than two values produce nothing. Use withPrevious when the first value matters, pairwise when only complete pairs do.
Concurrency¶
mapAsync preserves upstream order¶
import kotlinx.coroutines.flow.asFlow
userIds.asFlow()
.mapAsync(concurrency = 4) { id -> api.fetchUser(id) }
.collect { user -> println(user.name) }
// Up to 4 fetches run at once; results arrive in userIds order.
A result is emitted only after the results of all earlier values have been emitted, so one slow transform delays later results. The upstream is collected only while permits are available, which bounds memory at concurrency pending results. A failure in the transform or the upstream cancels all in-flight work and fails the flow.
mapAsyncUnordered emits in completion order¶
imageUrls.asFlow()
.mapAsyncUnordered(concurrency = 4) { url -> downloadThumbnail(url) }
.collect { thumbnail -> cache.put(thumbnail) }
// Each thumbnail is emitted as soon as it is ready; output order can differ
// from upstream order.
Same concurrency bound and failure behavior as mapAsync. Prefer it when downstream work does not care about order, because no result waits behind a slower one.
Resilience¶
retryWithBackoff¶
priceUpdates
.retryWithBackoff(
retries = 5,
initialDelay = 200.milliseconds,
factor = 2.0,
maxDelay = 10.seconds,
retryIf = { it !is IllegalArgumentException },
)
.collect { render(it) }
// On failure the flow is re-collected after 200 ms, then 400 ms, 800 ms,
// 1.6 s, 3.2 s. Each delay is capped at maxDelay.
The delay before retry n (0-based) is initialDelay * factor.pow(n), capped at maxDelay. A failure is retried only while retryIf returns true for it. CancellationException is never retried, so cancelling the collector stops the flow immediately. Once retries are exhausted, the last failure is rethrown to the collector.
asResult, filterSuccess, filterFailure¶
locationUpdates
.asResult()
.collect { result ->
result
.onSuccess { location -> moveMarker(location) }
.onFailure { cause -> showBanner(cause) }
}
// Every value arrives as Result.success. A terminal upstream failure arrives
// as a single Result.failure, then the flow completes normally.
Collectors never observe an exception from the upstream. Cancellation is not wrapped and propagates as usual. Split a Flow<Result<T>> back into its parts with the two filters:
val results = locationUpdates.asResult()
results.filterSuccess() // Flow<Location>: successful values only
results.filterFailure() // Flow<Throwable>: failure causes only
timeoutBetweenEmissions¶
heartbeats
.timeoutBetweenEmissions(30.seconds)
.collect { registerBeat(it) }
// Fails with TimeoutCancellationException when 30 s pass without a beat.
// The gap between the start of collection and the first beat counts too.
Values already emitted are unaffected. On timeout the upstream is cancelled and the exception is thrown to the collector, so wrap the flow in retryWithBackoff or asResult if a silent stall should trigger recovery instead of a crash.
Composition¶
Stop on a signal with takeUntil¶
import kotlinx.coroutines.flow.MutableSharedFlow
val stop = MutableSharedFlow<Unit>()
intervalFlow(1.seconds)
.takeUntil(stop)
.collect { println(it) }
// 0, 1, 2, ... completes as soon as stop emits its first value.
If the signal completes without emitting, the upstream is mirrored to completion. Values are handed to the collector without buffering, so nothing is delivered after the signal fires. A failure in either flow fails the resulting flow.
Prefix values with startWith¶
val screenState = remoteArticles()
.map { articles -> UiState.Content(articles) }
.startWith(UiState.Loading)
// Collectors see Loading first, then Content once the network answers.
The vararg overload prefixes several values in order:
The prefix is emitted once per collection, before the upstream is collected.
Sequence flows with concatWith¶
cachedResults
.concatWith(networkResults)
.collect { showRow(it) }
// All cached rows first, then network rows.
The second flow is collected only after the upstream completes normally; if the upstream fails or the collector is cancelled, it is never collected. The vararg overload chains several flows, and a failure anywhere stops the concatenation.
Sample another flow with withLatestFrom¶
saveClicks
.withLatestFrom(formState) { _, form -> form }
.collect { form -> persist(form) }
// Each click captures the form as it was at click time. Clicks that arrive
// before formState has emitted are dropped.
Only the upstream drives emissions; new formState values alone produce nothing. The overload without a transform pairs the values:
combine6 to combine9¶
kotlinx.coroutines stops at five flows; KiteCore continues to nine with the same semantics:
val uiState = combine6(
user, cart, promotions, shipping, payment, connectivity,
) { u, c, promos, ship, pay, online ->
CheckoutState(u, c, promos, ship, pay, online)
}
// Emits after every input has emitted at least once, then re-emits whenever
// any input produces a new value.
Bursts of updates may be conflated; the transform always observes the latest values. combine7, combine8, and combine9 follow the same shape with more parameters.
Flatten collections with flattenIterable¶
pagedResults // Flow<List<Item>>
.flattenIterable() // Flow<Item>
.collect { item -> println(item.id) }
// Every element of every page, in order. Empty pages produce nothing.
Insert separators with intersperse¶
Flows with fewer than two values are emitted unchanged.
Gating and time limits¶
Skip early values with dropUntil¶
sensorReadings
.dropUntil(calibrationDone)
.collect { record(it) }
// Readings before calibrationDone emits are dropped; every reading after it
// is mirrored.
If the signal completes without emitting, every upstream value is dropped and the flow completes with the upstream. A failure in either flow fails the resulting flow.
Duration shorthands: takeFor and dropFor¶
prices.takeFor(10.seconds) // mirror the upstream for 10 s, then complete
prices.dropFor(10.seconds) // drop values for 10 s, then mirror
These are shorthands for takeUntil(timerFlow(duration)) and dropUntil(timerFlow(duration)).
Include the terminating value with takeWhileInclusive¶
downloadProgress
.takeWhileInclusive { it < 100 }
.collect { println(it) }
// ... 95, 98, 100 <- unlike takeWhile, 100 is emitted before completion
The upstream is cancelled once the predicate fails.
Builders¶
intervalFlow¶
intervalFlow(period = 1.seconds, initialDelay = 5.seconds)
.collect { tick -> println("tick $tick") }
// tick 0 after 5 s, then tick 1, tick 2, ... one per second.
The delay between emissions is measured from the completion of the previous emission, so a slow collector stretches the schedule instead of piling up values. The flow is infinite and completes only by cancellation.
countdownFlow¶
countdownFlow(from = 3, interval = 1.seconds)
.collect { println(it) }
// 3 (immediately), 2, 1, 0, then the flow completes.
timerFlow¶
flowFromSuspend and deferFlow¶
val profile = flowFromSuspend { api.loadProfile() }
// Cold: each collection invokes loadProfile() again and emits its one result.
val feed = deferFlow { if (session.isLoggedIn()) remoteFeed() else localFeed() }
// Cold: the provider runs at collection time, so each collector can receive
// a different flow.
flowFromSuspend wraps a value-producing call, deferFlow wraps a flow-producing call.
neverFlow¶
val idle: Flow<Nothing> = neverFlow()
// Never emits, never completes. Collection suspends until the collector is
// cancelled. Useful as an inert branch in a when expression that returns flows.
Indexed operators¶
Each indexed operator restarts its counter at 0 for every collection.
flowOf("a", "b", "c")
.mapIndexed { index, value -> "$index:$value" }
.collect { print("$it ") }
// 0:a 1:b 2:c
flowOf(10, 20, 30, 40)
.filterIndexed { index, _ -> index % 2 == 0 }
.collect { print("$it ") }
// 10 30
// The index counts every upstream value, including the filtered ones.
uploads
.onEachIndexed { index, file -> log("upload #$index: ${file.name}") }
.collect { send(it) }
// Values pass through unchanged; the action sees each value with its position.
Act on the first value with onFirst¶
messages
.onFirst { hideLoadingSpinner() }
.collect { append(it) }
// The action runs once, before the first value reaches the collector, and
// runs again on every new collection.
Count emissions with runningCount¶
errors
.runningCount()
.collect { count -> badge.text = "$count" }
// 1, 2, 3, ... an empty upstream produces nothing.
Discard values with mapToUnit¶
Terminal operators¶
These are suspend functions, not operators returning flows.
firstOrDefault and lastOrDefault¶
val theme = settingsFlow.firstOrDefault(Theme.System)
// Returns the first value and cancels the upstream. The default is used only
// when the flow completes without emitting; a null first value is returned
// as is for nullable T.
val finalScore = scores.lastOrDefault(0)
// Collects the flow to completion and returns the last value, or 0 when empty.
collectIn¶
val job = ticker.collectIn(scope) { render(it) }
// Shorthand for scope.launch { ticker.collect { render(it) } }.
Collection stops when the returned job or the scope is cancelled; an upstream failure fails the job through the scope.
StateFlow utilities¶
Derive state with mapState¶
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class CartModel {
private val items = MutableStateFlow<List<Item>>(emptyList())
val total: StateFlow<Int> = items.mapState { list -> list.sumOf { it.price } }
}
// total.value is always in sync with items.value. No coroutine, no scope,
// nothing to cancel.
The view is not backed by a coroutine: reading value applies the transform to the source's current value, and collection maps the source with equality conflation. The transform must be fast and pure, because it runs on every value read and every source emission.
Combine state with combineStates¶
val canSubmit: StateFlow<Boolean> =
combineStates(emailValid, passwordValid) { email, password -> email && password }
// Overloads accept 2, 3, or 4 source StateFlows.
Same contract as mapState: no backing coroutine, and the transform must be fast and pure.
Constant state with stateFlowOf¶
fun connectivity(): StateFlow<Boolean> = stateFlowOf(true)
// A StateFlow that always holds true. Collectors receive the value once and
// then suspend until cancelled. Useful as a fixed input where an API expects
// a StateFlow, for example in previews and tests.
Atomic updates: toggle, increment, decrement, getAndSet¶
val muted = MutableStateFlow(false)
val nowMuted = muted.toggle() // true; flips and returns the new value
val retries = MutableStateFlow(0)
retries.increment() // 1
retries.increment(by = 3) // 4
retries.decrement() // 3
val bytesSent = MutableStateFlow(0L)
bytesSent.increment(by = 4096L) // Long overloads mirror the Int ones
val selection = MutableStateFlow("none")
val previous = selection.getAndSet("all") // "none"; the swap is atomic
Every update is atomic with respect to concurrent updates: the function may be recomputed under contention but is applied exactly once. Collectors observe a new value only when it differs from the previous one, following MutableStateFlow equality conflation.
Reference¶
Every public declaration in io.github.yuroyami.kitecore.flow, alphabetically. Overloads share a row.
| Declaration | Summary |
|---|---|
asResult() |
Wraps values in Result.success and one terminal failure in Result.failure, then completes normally |
chunked(size) / chunked(window, maxSize) |
Batches values into lists by count, or by time window with an optional size cap |
collectIn(scope, action) |
Launches collection in a scope and returns the Job |
combine6(...) |
Combines the latest values of six flows |
combine7(...) |
Combines the latest values of seven flows |
combine8(...) |
Combines the latest values of eight flows |
combine9(...) |
Combines the latest values of nine flows |
combineStates(a, b[, c[, d]], transform) |
Coroutine-free combined StateFlow view of 2 to 4 sources |
concatWith(other) / concatWith(vararg) |
Appends other flows after normal completion of the upstream |
countdownFlow(from, interval) |
Counts down from a number to 0, then completes |
decrement(by) |
Atomically subtracts from a MutableStateFlow<Int> or <Long> and returns the new value |
deferFlow(provider) |
Obtains the actual flow from a suspending provider at collection time |
dropFor(duration) |
Drops values for a duration, then mirrors the upstream |
dropUntil(signal) |
Drops values until a signal flow emits, then mirrors the upstream |
filterFailure() |
Emits the exception of every failed Result, drops successes |
filterIndexed(predicate) |
Filters with access to each value's 0-based position |
filterSuccess() |
Emits the value of every successful Result, drops failures |
firstOrDefault(default) |
First value, or the default when the flow is empty; cancels the upstream |
flattenIterable() |
Emits every element of every upstream iterable, in order |
flowFromSuspend(block) |
Cold flow that emits the single result of a suspending call |
getAndSet(value) |
Atomically replaces a MutableStateFlow value and returns the previous one |
increment(by) |
Atomically adds to a MutableStateFlow<Int> or <Long> and returns the new value |
intersperse(separator) |
Emits a separator between consecutive values |
intervalFlow(period, initialDelay) |
Infinite flow of sequential numbers on a fixed period |
lastOrDefault(default) |
Last value, or the default when the flow is empty; collects to completion |
mapAsync(concurrency, transform) |
Concurrent mapping that preserves upstream order |
mapAsyncUnordered(concurrency, transform) |
Concurrent mapping that emits in completion order |
mapIndexed(transform) |
Maps with access to each value's 0-based position |
mapState(transform) |
Coroutine-free derived StateFlow view |
mapToUnit() |
Replaces every value with Unit |
neverFlow() |
Never emits, never completes |
onEachIndexed(action) |
Runs an action with each value and its position, passes values through |
onFirst(action) |
Runs an action with the first value only, passes all values through |
pairwise() / pairwise(transform) |
Emits consecutive pairs, or a transform of each pair |
retryWithBackoff(retries, initialDelay, factor, maxDelay, retryIf) |
Re-collects a failed upstream with exponential delays |
runningCount() |
Emits the running number of values collected so far, starting at 1 |
startWith(value) / startWith(vararg) |
Prefixes values before the upstream |
stateFlowOf(value) |
StateFlow that always holds one value |
takeFor(duration) |
Mirrors the upstream for a duration, then completes |
takeUntil(signal) |
Mirrors the upstream until a signal flow emits, then completes |
takeWhileInclusive(predicate) |
Like takeWhile, but also emits the value that fails the predicate |
throttleFirst(window) |
Emits the first value per window, drops the rest |
throttleLatest(window) |
Emits the newest value once per window |
timeoutBetweenEmissions(timeout) |
Fails when the gap between emissions exceeds the timeout |
timerFlow(delay) |
Emits one Unit after a delay, then completes |
toggle() |
Atomically flips a MutableStateFlow<Boolean> and returns the new value |
windowed(size, step) |
Emits sliding windows of full lists |
withLatestFrom(other[, transform]) |
Combines each upstream value with the latest value of another flow |
withPrevious() |
Pairs each value with its predecessor, null for the first |