Channels & Channel-Backed Flows Quiz
KOTLIN › Flow
What does send() do when the channel is full?
- It suspends the calling coroutine until space is available in the channel buffer
- It drops the oldest element to make room, like a ring buffer would do
- It throws BufferOverflowException immediately without suspending the coroutine at all
- It blocks the current thread until a receiver drains at least one element
Answer: It suspends the calling coroutine until space is available in the channel buffer
send() is a suspend function. When the buffer is full, it suspends (not blocks) the coroutine until another coroutine calls receive() and frees a slot.
What is the capacity of a Channel<Int>() created with no arguments?
- 0 (rendezvous): send always suspends until a matching receive is ready
- 64 (the default BUFFERED size that kotlinx.coroutines ships with)
- 1 (a single-element buffer that suspends only on the second unread send)
- Unlimited: the buffer grows dynamically and send never suspends at all
Answer: 0 (rendezvous): send always suspends until a matching receive is ready
The default is Channel.RENDEZVOUS (capacity 0). Every send() suspends until a receive() is waiting, and vice versa. You must explicitly pass a capacity or use Channel.BUFFERED / Channel.CONFLATED / Channel.UNLIMITED for other modes.
What does Channel.CONFLATED do when a new element is sent before the previous one is received?
- It overwrites the buffered element with the new one, so the receiver only sees the latest value
- It queues both elements in insertion order behind each other so no data is ever lost in the pipeline
- It suspends the sender until the receiver has consumed the older element first
- It throws ClosedSendChannelException because the single slot is already occupied
Answer: It overwrites the buffered element with the new one, so the receiver only sees the latest value
A conflated channel keeps only the most recent element. Older unread values are silently dropped. This is useful when only the latest state matters, like UI updates.
What does produce {} return and what happens when its lambda completes?
- A ReceiveChannel that is automatically closed when the producer block finishes
- A SendChannel that the caller must close manually after all items have been sent
- A Flow that replays the produced elements to every new collector that subscribes
- A Deferred that resolves to the list of all produced elements once the block returns
Answer: A ReceiveChannel that is automatically closed when the producer block finishes
produce {} is a coroutine builder that returns ReceiveChannel<T>. When the block completes, the channel is closed automatically. This is safer than manually creating and closing a channel.
Two coroutines both call receive() on the same channel. What happens when a value is sent?
- Exactly one receiver gets the value; the other stays suspended until the next send
- Both receivers get a copy of the value, since channels broadcast to all consumers
- The channel throws IllegalStateException because multiple receivers are not allowed
- The value is dropped because the channel cannot determine which receiver should win
Answer: Exactly one receiver gets the value; the other stays suspended until the next send
Channels deliver each element to exactly one receiver (fan-out). There is no broadcast. If you need every consumer to see every element, use SharedFlow or BroadcastChannel (deprecated).
What is the difference between send() and trySend()?
- send() suspends when the buffer is full; trySend() returns immediately with a result
- send() is thread-safe while trySend() requires external synchronization to be safe
- trySend() throws on failure while send() silently drops the element on a full channel
- send() works only inside produce {} while trySend() works from any coroutine scope
Answer: send() suspends when the buffer is full; trySend() returns immediately with a result
send() is a suspend function: it waits for capacity. trySend() is non-suspending and returns a ChannelResult (success/failure/closed). Use trySend() from callbacks or other non-suspend contexts.
Why are channels described as 'hot' compared to flows which are 'cold'?
- A channel exists and can buffer data whether or not anyone is receiving from it right now
- Channels run on a dedicated background thread while flows always share the caller's thread pool
- Hot means the channel uses more memory because it pre-allocates its entire buffer at creation
- Channels automatically retry failed sends while flows propagate errors to the collector only
Answer: A channel exists and can buffer data whether or not anyone is receiving from it right now
A channel is hot: once created, producers can send into it regardless of consumers. A flow is cold: nothing executes until collect() is called. This is the fundamental difference in their execution model.
When should you use channelFlow {} instead of flow {}?
- When you need to emit values from multiple concurrent coroutines inside the flow builder
- When you want the flow to buffer its output so the collector never suspends on slow emissions
- When the flow must survive configuration changes and outlive the collecting lifecycle owner
- When you need to emit values synchronously without suspending, which flow {} cannot do at all
Answer: When you need to emit values from multiple concurrent coroutines inside the flow builder
flow {} runs in a single coroutine and emit() must be called from that coroutine. channelFlow {} gives you a ProducerScope where you can launch child coroutines that all call send(). It is cold (like flow) but internally backed by a channel for concurrent emission.
What happens when you call receive() on a closed, empty channel?
- It throws ClosedReceiveChannelException because the channel is both closed and drained
- It returns null to signal that no more elements will ever arrive from the closed producer side
- It suspends indefinitely waiting for a new element, since close is merely advisory
- It returns the last element that was sent before the channel was closed, as a fallback
Answer: It throws ClosedReceiveChannelException because the channel is both closed and drained
After close(), buffered elements can still be received. Once drained, receive() throws ClosedReceiveChannelException. Use receiveCatching() for a non-throwing alternative that returns ChannelResult.
What does select {} do in the context of channels?
- It suspends until the first of several channel operations is ready, then executes that branch
- It rounds-robin across channels, reading one element from each in a deterministic fixed order
- It merges all listed channels into a single combined channel that the caller can receive from
- It filters elements from a channel, selecting only those matching a predicate function guard clause
Answer: It suspends until the first of several channel operations is ready, then executes that branch
select {} is a suspending multiplexer. You list channel operations (onReceive, onSend) as clauses. It suspends until one is ready, executes that clause's lambda, and returns its result. It is like a suspending switch statement over channel readiness.
What was the actor {} builder used for, and why is it deprecated?
- It created a coroutine with its own mailbox channel, replaced by safer patterns like shared flows
- It spawned a dedicated thread for CPU-intensive work, replaced by Dispatchers.Default instead
- It broadcasted events to multiple consumers, replaced by SharedFlow which handles replay better
- It ran blocking I/O in a coroutine, replaced by withContext(Dispatchers.IO) for thread safety reasons
Answer: It created a coroutine with its own mailbox channel, replaced by safer patterns like shared flows
actor {} combined a coroutine with a SendChannel mailbox, serializing messages for safe state mutation. It was deprecated because the pattern is fragile (easy to leak the channel) and SharedFlow / MutableStateFlow cover most use cases more safely.
How does consumeEach {} differ from a for loop over a channel?
- It cancels the channel's producer on any exception, while a for loop just stops iterating and leaks it
- It reads elements in batches of N for throughput, while a for loop reads them one by one sequentially
- It resumes from the last consumed element after a crash, while a for loop starts over from scratch
- It converts the channel into a flow and collects it, while a for loop uses the channel iterator protocol
Answer: It cancels the channel's producer on any exception, while a for loop just stops iterating and leaks it
consumeEach cancels the underlying ReceiveChannel if the block throws, ensuring the producer coroutine is cancelled and resources are freed. A bare for loop does not cancel the channel, so the producer keeps running on failure.
What is the fan-in pattern with channels?
- Multiple producer coroutines all send into one shared channel that a single consumer reads from
- One producer sends the same element to multiple channels simultaneously via a broadcast mechanism
- A single channel automatically load-balances its output across a pool of consumer coroutines
- Multiple channels are merged into a single flow using the combine operator for synchronized reads
Answer: Multiple producer coroutines all send into one shared channel that a single consumer reads from
Fan-in means many-to-one: several producers send into the same channel, and one (or more) consumers receive from it. The channel serializes all the sends into a single ordered stream.
What is the main risk of using Channel.UNLIMITED?
- The buffer grows without bound, so a fast producer can cause an OutOfMemoryError crash
- Elements are silently dropped when the JVM runs low on memory, with no failure notification
- It forces every send to allocate a new coroutine, which exhausts the dispatcher's thread pool
- Receivers must consume elements within a timeout or the channel is automatically closed by the runtime
Answer: The buffer grows without bound, so a fast producer can cause an OutOfMemoryError crash
UNLIMITED means the internal buffer has no cap. send() never suspends, so a producer that outpaces its consumer fills memory indefinitely. Prefer BUFFERED or RENDEZVOUS for back-pressure.
You wrap a callback-based location API as a Flow using callbackFlow. What must the builder block do at the end to behave correctly?
- Return the last emitted value as the block's result and finish
- Call awaitClose { } to wait for cancel and remove the callback
- Call flowOn(Dispatchers.IO) as the final statement in the block
- Throw CancellationException manually to stop the stream early
Answer: Call awaitClose { } to wait for cancel and remove the callback
callbackFlow requires awaitClose at the end; it keeps the producer alive until the collector cancels and provides a place to remove the listener. Omitting it throws IllegalStateException at runtime.
A ViewModel exposes _events.consumeAsFlow() and the UI collects it inside repeatOnLifecycle. What breaks?
- Backgrounding ends the collection, which cancels the channel, so no further events arrive
- Each event is delivered twice, once per lifecycle restart
- Events are buffered and then replayed again on every single return to the foreground
- Nothing: this is the recommended pattern
Answer: Backgrounding ends the collection, which cancels the channel, so no further events arrive
consumeAsFlow cancels the channel when its single collection ends, and repeatOnLifecycle ends it on every backgrounding. receiveAsFlow leaves the channel intact.
What is the difference between close() and cancel() on a channel?
- close() lets buffered elements drain; cancel() discards them
- close() discards buffered elements; cancel() drains them
- They are synonyms, differing only in the exception raised on send
- close() affects senders only; cancel() affects receivers only
Answer: close() lets buffered elements drain; cancel() discards them
close is the producer saying it has finished, so the consumer loop ends naturally once the buffer empties. cancel is an abort, and pending receivers get a CancellationException.
A callbackFlow uses trySend on a default-capacity channel. What happens to values when no receiver is waiting?
- They fail silently, because RENDEZVOUS has no buffer and trySend never suspends
- They suspend the callback until the collector is finally ready to receive them
- They are queued up to 64 elements
- They raise ClosedSendChannelException
Answer: They fail silently, because RENDEZVOUS has no buffer and trySend never suspends
trySend returns a ChannelResult rather than throwing, so a failed send is invisible unless you check it. Giving the channel a buffer is the usual fix.
Four coroutines each iterate the same channel with for (job in channel). What happens?
- Each element goes to exactly one worker, load-balancing the queue
- Every worker receives every element
- Only the first worker receives anything
- The channel throws, since it only ever allows one single receiver
Answer: Each element goes to exactly one worker, load-balancing the queue
Fan-out splits work rather than broadcasting it, and a worker that finishes early takes the next item. That is the property that distinguishes a channel from a SharedFlow.
In a fan-in, where should close() be called?
- In a coroutine that joins every forwarder first, so it happens exactly once
- Inside each forwarding coroutine when its source is exhausted
- By the consumer once it has read enough elements
- Nowhere: a fan-in channel closes itself once all of its senders have finished
Answer: In a coroutine that joins every forwarder first, so it happens exactly once
Closing inside each forwarder means the first to finish cuts off the rest, and never closing leaves the consumer loop waiting forever. A channel does not know how many senders it has.
Two coroutines collect the same receiveAsFlow(). What do they receive?
- They split the elements between them, since each element is delivered once
- Both receive every element, as with a SharedFlow
- The second collector throws IllegalStateException
- The second collector receives only the elements that were sent after it subscribed
Answer: They split the elements between them, since each element is delivered once
Channels deliver each element to exactly one receiver regardless of the wrapper. That is correct for a work queue and wrong for a broadcast, which is what SharedFlow is for.