Channels & Channel-Backed Flows Quiz

KOTLIN › Flow

What does send() do when the channel is full?

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?

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?

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?

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?

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

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

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

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?

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?

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?

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?

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?

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?

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?

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?

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?

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?

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?

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?

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?

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.

Back to Channels & Channel-Backed Flows