Irbisa · Cheatsheet
Kotlin Flow
Cold and hot streams, operators, StateFlow and SharedFlow, Channels, flowOn, backpressure, stateIn and shareIn, error handling
What is
Flow, and what is the difference between a cold and a hot stream?val numbers = flow { println("producer started") emit(1) emit(2)}…What is the difference between
FlowandSequence, and why can't you calldelayinside asequence { }builder?// Sequence: synchronous, the consumer pulls each valueval fib = sequence { var a = 0 var b = 1 while (true) { yield(a) // pauses until the next value is pulled…Which Flow operators are terminal, why does a chain that ends in
onEach { }do nothing, and what doeslaunchIn(scope)return?val prices = flowOf(1, 2, 3) .onEach { println("got $it") } .map { it * 10 } // nothing runs yet: no terminal operator suspend fun demo(scope: CoroutineScope) { println(prices.first()) // got 1, 10: 2 and 3 are never produced…What is the difference between
StateFlowandSharedFlow, and when do you use each?class CartVM : ViewModel() { private val _state = MutableStateFlow(CartState()) val state: StateFlow<CartState> = _state.asStateFlow() private val _messages = MutableSharedFlow<String>(extraBufferCapacity = 1) val messages: SharedFlow<String> = _messages.asSharedFlow()…Find the bug: tapping "Add" puts the item into the cart, but the list on screen never updates.
class CartViewModel : ViewModel() { private val items = mutableListOf<Item>() private val _cart = MutableStateFlow<List<Item>>(items) val cart: StateFlow<List<Item>> = _cart.asStateFlow() fun add(item: Item) { items.add(item) _cart.value = items }}Find the bug: the screen shows the user's name, but the unread count collected right below it never appears.
class InboxFragment : Fragment(R.layout.fragment_inbox) { private val vm: InboxViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val binding = FragmentInboxBinding.bind(view) viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { vm.userName.collect { binding.name.text = it } // StateFlow<String> vm.unreadCount.collect { binding.badge.text = "$it" } // StateFlow<Int> } } }}What is a
Channel, how does it differ from aFlow, and what do theRENDEZVOUS,BUFFERED,CONFLATEDandUNLIMITEDcapacities change forsend?fun main() = runBlocking { // A work queue: each job goes to exactly one worker val jobs = Channel<Int>(Channel.BUFFERED) repeat(2) { worker -> launch { for (job in jobs) { // ends when the channel is closed and drained…On which thread does each numbered step of this flow run, and how does
flowOnwork?// launched from viewModelScope (Dispatchers.Main)repo.rows() // flow { emit(db.readAll()) } .map { it.toUi() } // (1) .flowOn(Dispatchers.IO) .filter { it.isNotEmpty() } // (2) .flowOn(Dispatchers.Default) .collect { render(it) } // (3)Find the bug: this flow loads two pages in parallel and crashes with
IllegalStateException: Flow invariant is violated.fun feed(api: FeedApi): Flow<List<Post>> = flow { coroutineScope { launch { emit(api.page(1)) } // each page in its own coroutine launch { emit(api.page(2)) } }}What does this code print with
zip, and what withcombine? When would you usemergeinstead?val numbers = flow { for (n in 1..3) { delay(100); emit(n) } } // at 100, 200, 300 msval letters = flow { delay(150); emit("A"); delay(100); emit("B") } // at 150, 250 ms fun main() = runBlocking { numbers.zip(letters) { n, l -> "$n$l" }.collect { print("$it ") } println() numbers.combine(letters) { n, l -> "$n$l" }.collect { print("$it ") }}flatMapConcat,flatMapMergeorflatMapLatest: which do you use for search-as-you-type, and what else does that pipeline need?@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)class SearchVM(private val repo: SearchRepo) : ViewModel() { private val query = MutableStateFlow("") fun onQueryChange(text: String) { query.value = text } val results: StateFlow<SearchState> = query…What is the difference between
debounce,sampleand throttle-first, and which one fits a search box, a sensor stream and a Pay button?// kotlinx.coroutines has no throttleFirst: a few lines with flow { }fun <T> Flow<T>.throttleFirst(window: Duration): Flow<T> = flow { var opened: TimeMark? = null // per collector: state lives inside flow { } collect { value -> val mark = opened if (mark == null || mark.elapsedNow() >= window) {…A Flow emits faster than the collector can process. What happens by default, and how do
buffer,conflateandcollectLatestchange it?val source = flow { repeat(3) { delay(100); emit(it) } // 100 ms to produce each} suspend fun compare() { // default: about 900 ms, producer waits for the collector…Why doesn't
catchsee the exception thrown in yourcollectblock, and where doretryandcatchgo in a flow chain?class FeedVM(private val repo: FeedRepo) : ViewModel() { val feed: StateFlow<FeedState> = repo.feedFlow() .retryWhen { cause, attempt -> if (cause !is IOException || attempt >= 3) return@retryWhen false delay(500L * (1 shl attempt.toInt())) // 0.5 s, 1 s, 2 s true // re-collect the upstream…How do
onStart,onCompletionandonEmptywork, and where do you clear a loading flag so it also clears on error and cancellation?class SyncController(private val repo: SyncRepository, private val scope: CoroutineScope) { val state = MutableStateFlow(SyncUiState()) fun start(): Job = repo.sync() // Flow<SyncProgress>, completes when done .onStart { state.update { it.copy(isSyncing = true, error = null) } } .onEach { p -> state.update { it.copy(percent = p.percent) } }…When should a repository function return
Flow<T>, and when asuspend funthat returnsT?What do
stateInandshareIndo, and what doesSharingStarted.WhileSubscribed(5_000)actually control?class NotesVM(private val repo: NoteRepo) : ViewModel() { // right: built once, one upstream collection for the ViewModel's life val notes: StateFlow<List<Note>> = repo.notesFlow() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000),…Where should a screen's first load start: in the ViewModel's
init { }, in aLaunchedEffect, or in a flow exposed withstateIn? What does each cost?class ProfileViewModel(private val repo: ProfileRepository) : ViewModel() { private val retries = MutableStateFlow(0) @OptIn(ExperimentalCoroutinesApi::class) val state: StateFlow<ProfileUiState> = retries .flatMapLatest {…One slow subscriber of a
MutableSharedFlowevent bus stalls the other subscribers and the emitter. Why, and what doextraBufferCapacityandonBufferOverflowchange?fun main() = runBlocking { val bus = MutableSharedFlow<Int>() // replay 0, extraBufferCapacity 0 launch { bus.collect { delay(300); println("slow got $it") } } launch { bus.collect { println("fast got $it") } } delay(50) // let both subscribe repeat(4) { bus.emit(it) } // returns only after slow takes 3…How is
Flowimplemented under the hood (Flow,FlowCollector), and how would you write your own operator?// The whole contract, simplified from kotlinx.coroutines:// interface Flow<out T> { suspend fun collect(collector: FlowCollector<T>) }// fun interface FlowCollector<in T> { suspend fun emit(value: T) } // A custom operator: each value paired with the one before itfun <T : Any> Flow<T>.pairwise(): Flow<Pair<T, T>> = flow {…