Skip to content

Concurrency


What is structured concurrency?

intermediate coroutines concurrency kotlin
View Answer

Structured concurrency is a coroutine design principle where child coroutines are tied to a parent scope.

This ensures:

  • automatic cancellation

  • lifecycle-aware execution

  • proper error propagation

  • prevention of coroutine leaks

Core concepts:

  • CoroutineScope

  • Job hierarchy

  • parent-child relationships

  • cooperative cancellation

๐Ÿš€ See Full Deep Dive


What is a suspend function?

beginner coroutines kotlin concurrency
View Answer

A suspend function can suspend execution without blocking a thread.

It can only be called from another suspend function or coroutine.

Key points:

  • not necessarily asynchronous by itself

  • compiled into coroutine machinery

  • can resume later after suspension

  • used for non-blocking async work

๐Ÿš€ See Full Deep Dive


What is a Continuation in Kotlin coroutines?

senior coroutines runtime kotlin
View Answer

A Continuation represents the rest of a suspended computation.

When a suspend function pauses, Kotlin stores state in a continuation so it can resume later from the correct point.

Interview points:

  • coroutine state is captured in the continuation

  • used by suspend functions and builders

  • enables non-blocking suspension/resumption

๐Ÿš€ See Full Deep Dive


How does coroutine suspension work internally?

senior coroutines compiler internals
View Answer

The Kotlin compiler rewrites suspend code into a resumable state machine.

This transformation adds labels/state transitions and continuation handling around each suspension point.

Why it matters:

  • explains how suspend functions resume

  • helps reason about allocation and debugging

  • is the basis for coroutine CPS transformation

๐Ÿš€ See Full Deep Dive


What is the difference between threads and coroutines?

beginner threading coroutines concurrency
View Answer

Threads are OS-level execution units; coroutines are lightweight units of suspended work scheduled on threads.

Practical distinction:

  • threads are heavier to create/switch

  • coroutines are cheaper and structured

  • multiple coroutines can share a small thread pool

  • coroutines do not replace threads for every use case

๐Ÿš€ See Full Deep Dive


What are Dispatchers in Kotlin Coroutines?

intermediate coroutines dispatchers threading
View Answer

Dispatchers decide where coroutine code runs.

Common dispatchers:

  • Main for UI work

  • IO for blocking I/O

  • Default for CPU-heavy work

Good interview answer: dispatchers are a scheduling policy, not a background-thread guarantee by themselves.

๐Ÿš€ See Full Deep Dive


What does withContext do and why is it important?

intermediate coroutines threading kotlin
View Answer

withContext switches coroutine execution to another dispatcher for a scoped block of work.

It is useful for:

  • moving blocking work off the main thread

  • returning results back to the original coroutine

  • keeping context switching explicit and readable

๐Ÿš€ See Full Deep Dive


What is CoroutineScope and why does it matter?

beginner coroutines scope android
View Answer

CoroutineScope defines the lifetime boundary for launching coroutines.

It matters because it controls:

  • cancellation ownership

  • structured concurrency behavior

  • leak prevention

  • lifecycle alignment in Android

๐Ÿš€ See Full Deep Dive


How does coroutine job hierarchy work?

intermediate coroutines job structure
View Answer

Coroutine jobs form a parent-child hierarchy.

When a parent job is cancelled, its children are cancelled too. Child failure can also propagate upward depending on scope type.

Interview points:

  • explicit ownership tree

  • cooperative cancellation

  • failure propagation rules

๐Ÿš€ See Full Deep Dive


What is the difference between Job and SupervisorJob?

senior coroutines job failure
View Answer

A regular Job propagates child failure to the parent and can cancel siblings.

SupervisorJob isolates child failures so one child can fail without automatically cancelling others.

Use it when:

  • sibling tasks are independent

  • partial failure should not bring down the whole scope

๐Ÿš€ See Full Deep Dive


What does supervisorScope do?

senior coroutines scope failure
View Answer

supervisorScope creates a scoped region where child failures are isolated.

It is useful when you want to launch multiple related tasks and tolerate failure in one child while handling its result explicitly.

Difference from coroutineScope:

  • failure isolation vs fail-fast cancellation

  • localized supervision boundary

๐Ÿš€ See Full Deep Dive


How does coroutine cancellation work?

intermediate coroutines cancellation concurrency
View Answer

Coroutine cancellation is cooperative.

A coroutine stops when it checks for cancellation or reaches a cancellable suspension point.

Important ideas:

  • Job.cancel() requests cancellation

  • suspension functions often detect it automatically

  • non-cooperative work must check manually

๐Ÿš€ See Full Deep Dive


What is cooperative cancellation?

beginner coroutines cancellation performance
View Answer

Cooperative cancellation means coroutines must periodically observe cancellation and stop themselves instead of being forcefully killed.

This is important because it keeps cleanup predictable and avoids abrupt state corruption.

Common tools:

  • isActive

  • ensureActive()

  • cancellable suspending functions

๐Ÿš€ See Full Deep Dive


How are exceptions handled in coroutines?

senior coroutines exceptions supervision
View Answer

Coroutine exception handling depends on the coroutine builder and scope.

Key points:

  • launch propagates exceptions to its parent

  • async stores exceptions until awaited

  • failures can cancel sibling children in regular scopes

  • supervision changes propagation behavior

๐Ÿš€ See Full Deep Dive


What is CoroutineExceptionHandler used for?

senior coroutines exceptions android
View Answer

CoroutineExceptionHandler handles uncaught exceptions in root coroutines launched with launch.

It is not a replacement for try/catch in suspend code.

Use it to:

  • log/report uncaught failures

  • provide a last-resort safety net

  • centralize crash telemetry in root scopes

๐Ÿš€ See Full Deep Dive


What is the difference between launch and async?

intermediate coroutines builders concurrency
View Answer

launch is for fire-and-forget work; async is for concurrent work that produces a result.

Interview framing:

  • launch returns Job

  • async returns Deferred

  • async exceptions are observed on await()

  • prefer the simplest builder that matches the need

๐Ÿš€ See Full Deep Dive


What is lazy async?

senior coroutines async scheduling
View Answer

Lazy async defers coroutine start until the result is actually awaited or explicitly started.

It can be useful when you want to create a deferred task graph but avoid doing work until the result is needed.

Tradeoff: lazy start can make control flow harder to reason about.

๐Ÿš€ See Full Deep Dive


How do you limit coroutine parallelism?

senior coroutines performance parallelism
View Answer

Limit parallelism by controlling dispatcher usage and the number of concurrently active jobs.

Common approaches:

  • semaphore-style gating

  • dispatcher limitedParallelism

  • batching work into controlled chunks

This prevents resource saturation and tail-latency spikes.

๐Ÿš€ See Full Deep Dive


What are coroutine thread pools?

intermediate coroutines threading scheduler
View Answer

Coroutine dispatchers usually run on top of shared thread pools.

These pools multiplex many coroutines onto fewer OS threads, improving efficiency compared with one-thread-per-task models.

Interview points:

  • pools are shared resources

  • pool saturation affects latency

  • CPU and blocking work should be separated carefully

๐Ÿš€ See Full Deep Dive


What is thread starvation in concurrency?

senior threading scheduler performance
View Answer

Thread starvation happens when tasks cannot get scheduled time on a thread or thread pool because resources are monopolized by other work.

In Android apps it often appears when:

  • blocking code occupies shared pools

  • parallelism is unbounded

  • main-thread work is overloaded

๐Ÿš€ See Full Deep Dive


What is limitedParallelism in coroutines?

senior coroutines parallelism performance
View Answer

limitedParallelism constrains the amount of concurrent work scheduled through a dispatcher view.

It is useful for:

  • protecting shared downstream resources

  • preventing resource thrash

  • balancing throughput and fairness

Good interview answer: it is a concurrency control tool, not just an optimization.

๐Ÿš€ See Full Deep Dive


What is Flow in Kotlin?

beginner flow coroutines streams
View Answer

Flow is Kotlin's asynchronous stream API.

It is used to represent values that arrive over time while remaining cancellation-aware and coroutine-based.

Key traits:

  • declarative stream pipeline

  • cold by default

  • integrates with coroutines and operators

๐Ÿš€ See Full Deep Dive


What is the difference between cold and hot flows?

intermediate flow coroutines state
View Answer

Cold flows start producing values per collector. Hot flows can emit independently of collectors.

In interviews, explain:

  • cold = collector drives execution

  • hot = producer exists independently

  • StateFlow and SharedFlow are hot stream primitives

๐Ÿš€ See Full Deep Dive


What is backpressure in Flow?

senior flow performance buffering
View Answer

Backpressure is what happens when upstream produces data faster than downstream can consume it.

Flow handles this through suspension, buffering, conflation, or explicit operator choices.

Why it matters:

  • protects memory and responsiveness

  • affects latency and throughput

  • can change user-visible behavior

๐Ÿš€ See Full Deep Dive


When should you use collectLatest?

intermediate flow cancellation operators
View Answer

Use collectLatest when new emissions should cancel the previous collector work and only the newest value matters.

Common use cases:

  • search suggestions

  • rapid UI updates

  • debounced rendering pipelines

๐Ÿš€ See Full Deep Dive


When should you use flatMapLatest?

senior flow operators cancellation
View Answer

flatMapLatest switches to a new inner flow whenever the outer value changes, cancelling the previous inner stream.

This is ideal for:

  • query-driven network search

  • live UI filters

  • state pipelines where only the newest source matters

๐Ÿš€ See Full Deep Dive


What is buffering and conflation in Flow?

senior flow buffering performance
View Answer

Buffering allows upstream and downstream to proceed with some decoupling. Conflation drops intermediate values and keeps the latest one.

Use cases:

  • buffering for throughput

  • conflation for UI state that only needs the latest value

Tradeoff: speed vs completeness of intermediate emissions.

๐Ÿš€ See Full Deep Dive


What is the difference between StateFlow and SharedFlow?

intermediate flow state events
View Answer

StateFlow represents state with a current value. SharedFlow represents shared emissions/events without a required initial state.

Good interview framing:

  • StateFlow for persistent UI state

  • SharedFlow for one-off events or broadcasts

  • both are hot and coroutine-friendly

๐Ÿš€ See Full Deep Dive


When should you use a Channel instead of SharedFlow?

senior flow channels events
View Answer

Use a Channel when you want point-to-point handoff semantics or a queue-like one-consumer model.

Use SharedFlow when you want multicasting or multiple subscribers.

Interview nuance:

  • Channel = delivery semantics and backpressure control

  • SharedFlow = broadcast-style hot stream

๐Ÿš€ See Full Deep Dive


What are stateIn and shareIn used for?

senior flow sharing state
View Answer

stateIn converts a Flow into a hot StateFlow-like state holder. shareIn shares upstream work among multiple collectors.

They are useful when:

  • expensive upstream work should be shared

  • UI needs lifecycle-friendly hot stream behavior

  • a cold stream should become reusable across collectors

๐Ÿš€ See Full Deep Dive


How do you model one-off events with SharedFlow?

intermediate flow events android
View Answer

SharedFlow is often used to emit transient UI events such as navigation, toast messages, or snackbar actions.

Good practice:

  • keep replay small or zero for one-off events

  • collect from lifecycle-aware UI scopes

  • avoid encoding events as sticky state

๐Ÿš€ See Full Deep Dive


What is callbackFlow?

intermediate flow callbacks interop
View Answer

callbackFlow bridges callback-based APIs into a Flow.

It is useful when an API delivers values through listeners, observers, or callbacks rather than suspend/Flow primitives.

Key concerns:

  • close resources in awaitClose

  • avoid leaking listeners

  • respect backpressure and cancellation

๐Ÿš€ See Full Deep Dive


What is channelFlow?

senior flow channels concurrency
View Answer

channelFlow is a builder for producing Flow values from multiple concurrent senders inside a coroutine scope.

It is useful when emissions come from several concurrent sources and you need a single Flow channel to merge them safely.

๐Ÿš€ See Full Deep Dive


How do you bridge callbacks into Flow safely?

senior flow callbacks android
View Answer

Bridge callbacks into Flow with a lifecycle-aware, cancellable adapter.

Practical rules:

  • register listener when collection starts

  • unregister in awaitClose or equivalent cleanup

  • avoid blocking callback threads

  • keep backpressure and cancellation in mind

๐Ÿš€ See Full Deep Dive


What is Mutex in Kotlin coroutines?

intermediate synchronization coroutines concurrency
View Answer

Mutex is a coroutine-friendly mutual exclusion primitive.

It protects critical sections without blocking a thread the way a JVM lock might.

Interview points:

  • suspending lock acquisition

  • cooperative with coroutines

  • useful for protecting shared mutable state

๐Ÿš€ See Full Deep Dive


What are common synchronization strategies in concurrent code?

senior synchronization threading performance
View Answer

Synchronization strategies include mutexes, atomics, thread confinement, immutable data, and message passing.

Good interview answer:

  • choose the simplest strategy that preserves correctness

  • prefer immutability when practical

  • use locks only around real shared mutable state

๐Ÿš€ See Full Deep Dive


Why is shared mutable state dangerous?

intermediate concurrency state race-conditions
View Answer

Shared mutable state is hard to reason about because concurrent reads and writes can interleave unpredictably.

Risks include:

  • race conditions

  • inconsistent reads

  • deadlocks and contention

  • difficult debugging

๐Ÿš€ See Full Deep Dive


What are atomic operations used for?

senior concurrency atomic synchronization
View Answer

Atomic operations allow safe lock-free updates for certain shared values.

They are useful when you need:

  • simple counters or flags

  • high-throughput coordination

  • predictable low-overhead synchronization

They are not a universal replacement for locks.

๐Ÿš€ See Full Deep Dive


What is thread confinement?

senior threading concurrency architecture
View Answer

Thread confinement means restricting access to mutable state to a single thread or dispatcher.

This simplifies correctness because only one execution context can mutate the state at a time.

Common Android examples:

  • main-thread UI ownership

  • single-threaded executors for caches

๐Ÿš€ See Full Deep Dive


What is a race condition?

intermediate concurrency bugs threading
View Answer

A race condition happens when the behavior depends on the timing/order of concurrent operations.

In practice it appears as:

  • inconsistent UI or state

  • flaky tests

  • intermittent crashes or stale data

๐Ÿš€ See Full Deep Dive


What is a deadlock?

senior concurrency deadlocks synchronization
View Answer

A deadlock occurs when two or more tasks are waiting on each other and none can continue.

Avoid deadlocks by:

  • minimizing lock scope

  • using consistent lock ordering

  • preferring non-blocking coordination when possible

๐Ÿš€ See Full Deep Dive


How do you test coroutines?

intermediate testing coroutines concurrency
View Answer

Test coroutines with a controlled test dispatcher and coroutine test scope.

Good practices:

  • replace real dispatchers with test dispatchers

  • assert state transitions deterministically

  • avoid real delays and sleeps in tests

๐Ÿš€ See Full Deep Dive


How does virtual time testing work?

senior testing coroutines time
View Answer

Virtual time testing advances coroutine-scheduled time without waiting in real time.

This makes delay-based and timeout-based tests fast and deterministic.

It is especially useful for:

  • retry logic

  • debounce behavior

  • cancellation timing

๐Ÿš€ See Full Deep Dive


Why use test dispatchers for coroutine tests?

intermediate testing dispatchers concurrency
View Answer

Test dispatchers let you control coroutine execution in tests.

This helps you:

  • avoid flaky scheduler-dependent behavior

  • make execution deterministic

  • verify concurrency logic without real threads or delays

๐Ÿš€ See Full Deep Dive


How do you debug coroutines in production?

senior debugging coroutines observability
View Answer

Debugging coroutines in production requires good tracing and context naming.

Helpful techniques:

  • name coroutine jobs meaningfully

  • log structured context and cancellation events

  • correlate coroutine lifecycles with user actions and requests

๐Ÿš€ See Full Deep Dive


How should you observe coroutine and Flow behavior?

senior observability flow concurrency
View Answer

Observe coroutines and flows by adding tracing around launch/collection, measuring latency, and tracking cancellations/failures.

Useful signals:

  • collection duration

  • emission count and rate

  • cancellation and error frequency

๐Ÿš€ See Full Deep Dive


How should Flow be collected with Android lifecycle?

intermediate android flow lifecycle
View Answer

Collect Flow with lifecycle awareness so UI stops observing when the screen is not active.

This prevents wasted work and leaks.

In Android, the common guidance is to tie collection to a lifecycle scope or repeat collection when the lifecycle is active.

๐Ÿš€ See Full Deep Dive


What does main-safety mean?

intermediate android main-thread concurrency
View Answer

Main-safety means code can be called from the main thread without blocking it for long periods.

A main-safe API internally shifts blocking work off the UI thread, keeping UI responsive.

๐Ÿš€ See Full Deep Dive


Why do blocking calls on the main thread cause ANRs?

senior android anr performance
View Answer

Blocking the main thread prevents input, rendering, and lifecycle work from progressing on time.

If the app cannot respond within the system's expected window, it can trigger an ANR.

The fix is to keep blocking work off the main thread and make APIs main-safe.

๐Ÿš€ See Full Deep Dive


How do you optimize concurrency performance in production?

senior performance concurrency coroutines
View Answer

Optimize concurrency by reducing contention, limiting parallelism, and minimizing blocking work.

Practical steps:

  • measure before tuning

  • keep main-thread work minimal

  • share expensive upstream work

  • use bounded pools and backpressure-aware flows

๐Ÿš€ See Full Deep Dive


Explain coroutine exception handling - try/catch, CoroutineExceptionHandler, and SupervisorJob

intermediate kotlin coroutines exception-handling supervisor
View Answer

Exception handling in coroutines differs from synchronous code because cancellation and failures propagate through the parent-child hierarchy.

In interviews, cover:

  • try/catch inside a coroutine: catches exceptions thrown by suspend functions normally โ€” this works but only within the coroutine that uses it

  • uncaught exceptions in launch: propagate to the parent coroutine and cancel the entire scope unless a SupervisorJob is involved; use CoroutineExceptionHandler as the last resort handler attached to the scope

  • CoroutineExceptionHandler: installed on the CoroutineScope or GlobalScope; called for uncaught exceptions from launch-based coroutines; NOT called for async-based ones (async propagates via Deferred.await())

  • async: wrap await() in try/catch; the exception is rethrown at await() time; CoroutineExceptionHandler does not help here

  • SupervisorJob: child failures do not cancel siblings; use supervisorScope { } for independent parallel tasks where one failing should not kill all others

Strong answer tip:

  • CancellationException is special โ€” it should never be caught and swallowed; always rethrow it so coroutine cancellation works correctly

๐Ÿš€ See Full Deep Dive


Compare Channels vs Flow vs SharedFlow โ€” how to choose the right primitive

advanced kotlin coroutines channels flow sharedflow
View Answer

Choosing between Channel, Flow, and SharedFlow depends on delivery guarantees, subscriber count, and lifecycle management requirements.

In interviews, cover:

  • cold Flow: lazy, unicast, each collector gets its own sequence; perfect for repository data that should start fresh per subscriber; no buffering by default

  • SharedFlow: hot, multicast, can replay latest N values; use for events that multiple observers should observe simultaneously (e.g. UI + analytics); never loses emissions past the replay buffer

  • Channel: hot, point-to-point; only one consumer receives each item (unless you fan-out manually); suited for producer/consumer queues and work distribution between coroutines

  • StateFlow: special SharedFlow with replay=1 and conflation; the canonical holder of current UI state โ€” always holds a current value, deduplicated by equality

Strong answer tip:

  • the classic mistake is using Channel for UI events observed by multiple elements; use SharedFlow(replay=0) or a Channel wrapped in receiveAsFlow() for single-shot event delivery

๐Ÿš€ See Full Deep Dive


Explain cancellation cooperation in coroutines โ€” where it works and where it does not

intermediate kotlin coroutines cancellation
View Answer

Kotlin coroutines use cooperative cancellation โ€” a coroutine is only cancelled when it reaches a suspension point that checks cancellation status.

In interviews, cover:

  • cancellation is checked at every suspend function from the coroutines library (delay, yield, withContext, IO operations); pure CPU-bound loops without suspension never see cancellation

  • ensureActive() or isActive checks make CPU-intensive loops cancellable: while(isActive) { ... }

  • blocking calls (Thread.sleep, JDBC queries) block the thread and ignore cancellation; wrap with Dispatchers.IO and use yield() if the work is interruptible

  • withContext(NonCancellable) { ... }: use only for cleanup in a finally block that must run even after cancellation โ€” never use it to suppress cancellation of the main work

  • catching CancellationException and not re-throwing it breaks structured concurrency; always rethrow

Strong answer tip:

  • demonstrate: for(i in 0..1_000_000) { expensiveCompute(i) } will not respond to cancel; fix: for(i in 0..1_000_000) { ensureActive(); expensiveCompute(i) }

๐Ÿš€ See Full Deep Dive


How do withTimeout and withTimeoutOrNull work, and what are the UX correctness traps?

intermediate kotlin coroutines timeout withTimeout
View Answer

withTimeout cancels the block with TimeoutCancellationException when the deadline elapses; this propagates up unless caught, while withTimeoutOrNull returns null instead.

In interviews, cover:

  • withTimeout cancellation is a CancellationException subclass โ€” it propagates up and cancels the enclosing scope unless explicitly caught with try/catch(TimeoutCancellationException) or by using withTimeoutOrNull

  • the scope is cancelled after timeout but started work (e.g. a started network request) may not be cancelled if the underlying dispatcher or blocking I/O doesn't support interruption

  • UX trap: if a UI action timeout fires while the user is still interacting, partial state changes may have already happened; pair with proper rollback logic or use idempotent operations

  • withTimeoutOrNull is safer for optional data fetching where a fallback is acceptable

Strong answer tip:

  • distinguish: timeout inside a supervisorScope means only that child is cancelled; timeout inside a regular scope cancels all siblings โ€” this is frequently wrong at the UI layer

๐Ÿš€ See Full Deep Dive


Explain Dispatchers.Main.immediate and re-entrancy hazards in coroutines

intermediate kotlin coroutines dispatchers main-thread
View Answer

Dispatchers.Main.immediate executes a coroutine body directly without re-posting to the main looper if the call is already on the main thread, reducing one frame of latency.

In interviews, cover:

  • Dispatchers.Main posts a Runnable to the main message queue even if called from the main thread; Dispatchers.Main.immediate checks and runs inline if already on main โ€” this avoids an extra frame delay for UI updates

  • re-entrancy trap: if immediate execution causes a UI state update while a measure/layout/draw pass is in progress, the resulting re-draw may be dropped or cause visual glitches

  • preferred use: ViewModelScope uses Main.immediate by default in Jetpack so state updates are applied synchronously when possible without async overhead

  • distinguish from calling launch(Dispatchers.Main): always posts; launch(Dispatchers.Main.immediate): may run inline

Strong answer tip:

  • use Main.immediate for ViewModel โ†’ UI state pushes; avoid it for transitions or animations where you explicitly need the next frame boundary

๐Ÿš€ See Full Deep Dive


Explain state vs event modeling and the SingleLiveEvent replacement pattern

intermediate kotlin coroutines state events udf
View Answer

Conflating state and transient events in a StateFlow or LiveData causes events to replay on configuration change; the correct model separates durable state from consumed-once events.

In interviews, cover:

  • LiveData's SingleLiveEvent allowed one-shot delivery but was not multi-observer safe; with SharedFlow(replay=0), an emission is delivered to current collectors only and is never replayed

  • state: durable, always has a current value, should be replayed to new observers (e.g. loading/content/error enum); use StateFlow

  • events: consumed-once side effects (navigation, snackbar, dialog show); use SharedFlow(replay=0, extraBufferCapacity=1) or Channel.receiveAsFlow(); never put them in StateFlow

  • on rotation: the ViewModel is retained; a new collector subscribes and if state is in StateFlow, it receives the last value โ€” correct; if a one-shot event was in StateFlow, it re-fires โ€” incorrect

Strong answer tip:

  • the Compose-specific problem: LaunchedEffect(key) runs when key changes; if you use an ever-incrementing counter as a key for events, be careful about recomposition re-launching the effect

๐Ÿš€ See Full Deep Dive


Explain callbackFlow correctness - awaitClose, cancellation, and leak prevention

intermediate kotlin coroutines flow callbackflow
View Answer

callbackFlow bridges callback-based APIs to Flow, but incorrect cleanup in awaitClose causes callback leaks and undefined behavior on cancellation.

In interviews, cover:

  • callbackFlow { ... awaitClose { unregister() } }: awaitClose holds the flow open until the collector cancels; the lambda inside awaitClose must unregister the callback unconditionally

  • forgetting awaitClose or leaving it empty means the callback continues firing after the collector stops, creating a memory or resource leak

  • trySend() not send(): inside a callback, you cannot call send() (which is a suspend call); use trySend() or buffer the flow; if the buffer is full, data is dropped โ€” size the buffer based on expected callback frequency

  • concurrency: callbacks may fire from any thread; Flow's internal channel is thread-safe but ensure data classes passed out are immutable or defensively copied

Strong answer tip:

  • test cancellation explicitly: cancel the scope collecting the flow and verify via a mock that the callback was unregistered โ€” this is the most common callbackFlow bug in production

๐Ÿš€ See Full Deep Dive