This is Part 6 of the android interview question series. This part will focus on Kotlin flows.
1. What is Flow?
A stream of data that can be computed asynchronously is referred to as a Flow
. It allows you to emit multiple values over time in a sequential and reactive manner. Some key characteristics of Flow
:
Flow
is designed to handle asynchronous data streams, where values are emitted one after the other. Each emission is processed sequentially, suspending until the previous emission completes, providing a natural way to handle data flow in a non-blocking way.Flow
handles backpressure automatically by suspending emissions if the collector (consumer) is slow to process them. This prevents overwhelming the consumer and manages resource usage effectively.Flow
is “cold,” meaning it doesn’t produce or emit any values until it is actively collected. Each time you callcollect
on aFlow
, it starts from scratch, similar to how a function is called and executed anew. This is different from hot streams, likeLiveData
or RxJava’sSubject
, which emit values independently of whether there’s an active observer.
2. What are the different ways to create a Flow?
Flow builders allow you to create flows in various ways depending on your use case. The most commonly used flow builders include:
- The
flow
builder is the primary way to create a flow. It allows you to emit values asynchronously using theemit()
function.
fun simpleFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(100) // Simulate some delay
emit(i) // Emit values from 1 to 5
}
}
- The
flowOf
builder creates a flow from a fixed set of values.
val values = flowOf(1, 2, 3, 4, 5)
- The
asFlow
extension allows you to convert collections or sequences into flows.
val values = listOf(1, 2, 3).asFlow()
3. What are the two different types of Flows?
There are two different types of Flows:
- A Cold Flow in Kotlin is a flow that does not start emitting values until a collector actively starts collecting it. This means that each collector (or subscriber) gets its own instance of the flow, and the flow starts from scratch every time it is collected.
- Cold flows are “lazy,” so no work is done until there is a demand for data.
- Each collector receives its own independent stream of data. Each time a new collector subscribes, the flow starts from the beginning.
- Suitable for use cases where you want fresh data for each subscriber, such as database queries, network requests, or other repeatable sources.
- Hot Flow emit values independently of whether there are active collectors or not. Once started, a hot flow continuously produces data that is shared among all collectors. This behavior is similar to broadcasting: new subscribers (collectors) receive only the latest emissions but miss any past values emitted before they started collecting.
- All collectors receive data from the same ongoing flow, starting from the latest value at the time they subscribe.
- Emission does not restart for each new collector; it’s a single, shared source.
- Suitable for scenarios like UI state updates, event broadcasting, or shared state where all subscribers need access to the latest values.
4. SharedFlow
vs StateFlow
?
Both SharedFlow
and StateFlow
are types of hot flows that emit values to multiple subscribers and keep emitting even when no subscribers are actively collecting.
StateFlow
is a specialized hot flow designed to hold and emit state updates. It always has a single current value and emits the latest state to new collectors.- It holds a single, current value that can be read and updated directly. Changes to the value are updated immediately, and new collectors always receive the latest value upon subscription.
- Only the latest value is replayed to new collectors.
- Exposed as an immutable
StateFlow
, so external subscribers can read but not modify the value. - Commonly used in ViewModels to hold the UI state and expose it to the UI layer, such as with Android’s Jetpack Compose or LiveData replacements. Ideal for cases where a single source of truth (the current state) needs to be shared with multiple consumers, ensuring all consumers always have the most recent data.
SharedFlow
is a general-purpose hot flow that can emit events or shared data to multiple subscribers.- Unlike
StateFlow
,SharedFlow
is highly configurable, allowing you to control the number of past emissions that new subscribers will receive (replay) and set a buffer to handle emissions when there are no active collectors. SharedFlow
does not hold a single current value. Instead, it broadcasts emissions to all subscribers.- Allows you to define a buffer for values, which can prevent emissions from being lost if there are no active collectors or if collectors are slow.
- Best for representing events or streams of data that do not represent a continuous state (such as notifications, one-time actions, or events that multiple subscribers might need).
5. What are Terminal operators in Flow?
Terminal operators are operators that collect the values emitted by a flow and perform a final action on them. Terminal operators are responsible for starting the flow’s collection process, meaning that until a terminal operator is invoked, the flow remains cold and does not produce any values. Different types of terminal operators:
collect
: is used to receive each emitted value from the flow and perform a specified action on it.
flowOf(1, 2, 3).collect { value ->
println("Received: $value")
}
toList
collects all emitted values and stores them in aList
, returning the list when the flow completes. Useful when you want to gather all items from a flow into a list.
val resultList = flowOf(1, 2, 3).toList()
println(resultList) // Output: [1, 2, 3]
toSet
collects all emitted values into aSet
, eliminating duplicates, and returns the set when the flow completes.
val resultSet = flowOf(1, 2, 2, 3).toSet()
println(resultSet) // Output: [1, 2, 3]
first
returns the first value emitted by the flow and then immediately cancels further collection.firstOrNull
is similar but returnsnull
if the flow is empty.
val firstValue = flowOf(1, 2, 3).first()
println(firstValue) // Output: 1
last
collects all values and returns the last emitted value. If the flow is empty, it throws an exception.lastOrNull
returns the last emitted value ornull
if the flow is empty.
val lastValue = flowOf(1, 2, 3).last()
println(lastValue) // Output: 3
single
expects the flow to emit exactly one value. If the flow emits more than one value, it throws an exception.singleOrNull
returnsnull
if the flow is empty, and if there’s only one item, it returns that item. It throws an exception if the flow emits more than one item.
val singleValue = flowOf(42).single()
println(singleValue) // Output: 42
reduce
performs a reduction operation, accumulating values as they are emitted by the flow. This operator applies an accumulator function to combine values and returns the final accumulated result. It’s similar tofold
, butreduce
doesn’t take an initial value and starts with the first emitted value as the initial accumulator.
val sum = flowOf(1, 2, 3, 4).reduce { accumulator, value ->
accumulator + value
}
println(sum) // Output: 10
fold
is similar toreduce
, but it allows you to specify an initial value for the accumulation. This is useful if you want to start the accumulation with a specific value (e.g., an initial count or sum).
val sumWithInitial = flowOf(1, 2, 3, 4).fold(10) { accumulator, value ->
accumulator + value
}
println(sumWithInitial) // Output: 20
count
counts the number of items emitted by the flow that satisfy a given predicate and returns the count. If no predicate is provided, it counts all items emitted.
val count = flowOf(1, 2, 3, 4).count { it % 2 == 0 }
println(count) // Output: 2 (counts 2 and 4)
6. What does the launchIn
keyword do?
launchIn
collects the flow within a specific coroutine scope without blocking the calling coroutine. It is often used when working with hot flows and when you want to start collecting in a different coroutine scope. Unlike other terminal operators, launchIn
doesn’t wait for the flow to complete but runs it in a separate coroutine.
val scope = CoroutineScope(Dispatchers.Default)
flowOf(1, 2, 3)
.onEach { println("Received: $it") }
.launchIn(scope)
7. What is the difference between StateIn
and ShareIn
?
stateIn
and shareIn
are operators used to convert a Flow
into a hot flow that can be shared among multiple collectors. Both are commonly used for transforming cold flows into hot flows that keep data in memory and emit it to multiple subscribers.
The stateIn
operator converts a cold Flow
into a StateFlow
, which is a hot flow that retains the latest emitted value and always has a single current state.
- When a new collector starts collecting, it immediately receives the latest value held in
StateFlow
, even if it started after that value was emitted. - Since
StateFlow
must always have a value,stateIn
requires an initial value that will be emitted until the flow starts producing data. StateFlow
always retains the latest emitted value, making it ideal for state management where you need to hold a “single source of truth” that represents the current state.- New collectors receive the latest value immediately upon subscription, even if they subscribe after the value was emitted.
The shareIn
operator converts a cold Flow
into a SharedFlow
, which is a hot flow that can replay a specified number of past values to new collectors.
- Unlike
stateIn
,shareIn
allows you to configure how many values to replay (if any) and provides more flexibility for managing event-driven flows. - Since
SharedFlow
doesn’t retain a single latest value by default, it’s better for event-based data streams where the most recent state isn’t needed. SharedFlow
can be configured to replay a certain number of past values to new collectors, making it suitable for event streams or data that needs to be replayed partially.- Unlike
stateIn
,shareIn
doesn’t require an initial value, as it’s used for handling events rather than holding state.
8. How can we collect Flows in Jetpack Compose?
- Using
collectAsState
with StateFlow: ThecollectAsState
extension function is ideal for collecting aStateFlow
in a Jetpack Compose function. It converts theStateFlow
into a ComposeState
, which automatically re-composes the UI when the flow emits a new value.
- Using
collectAsStateWithLifecycle
for Lifecycle-Aware Collection: In scenarios where theFlow
may emit values while the composable is not in a visible lifecycle state (such as paused or stopped), it’s recommended to usecollectAsStateWithLifecycle
, which is lifecycle-aware and only collects when the composable is in an active lifecycle state.
- Using
LaunchedEffect
withcollect
for Event Flows: If you want to collectFlow
events that are notStateFlow
or that represent one-time events (such as navigation events or showing a toast), you can useLaunchedEffect
along withcollect
. This method allows you to collectFlow
values inside a composable without re-composing on every emission.
9. How can we handle backpressure when using flows?
Backpressure occurs when the producer emits items at a higher rate than the consumer can process, leading to potential issues like memory overflow or delayed processing. Kotlin’s Flow
API provides several operators and strategies for handling backpressure effectively:
- The
buffer
operator allows you to add a buffer to a flow, enabling the producer to emit items without waiting for each item to be processed by the consumer. This helps smooth out the differences between the production and consumption rates.
conflate
: If only the latest values matter, we can make use of theconflate
operator: this keeps only the most recent value, dropping any previous unprocessed values. This reduces the memory usage by discarding intermediate emissions when the consumer is slower.
zip
andcombine
: These operators merge emissions from multiple flows.zip
matches values pairwise, whilecombine
merges the latest values from each flow.
10. How can we cancel a flow?
Flow operates under the structured concurrency model of coroutines. So cancelling a Flow
is generally done by cancelling the coroutine that is collecting the flow. Since flows are cold and only emit values when they are actively collected, cancelling the coroutine effectively stops the flow collection and cancels any ongoing emissions.
11. What does the flowOn
keyword do?
The flowOn
operator is used to change the coroutine context of the upstream operations in a flow. This is particularly useful when you need to specify a different thread or dispatcher for specific parts of a flow pipeline, without affecting the downstream operations (like collection).
12. How can we combine multiple flows?
Common operators to combine multiple flows:
- The
zip
operator combines two flows into one by pairing each emission from one flow with the corresponding emission from the other flow. The resulting flow emits values as pairs or as a transformation based on a provided lambda function. The combination stops as soon as one of the flows completes.
- The
combine
operator takes the latest value from each flow and emits a new value whenever any of the flows emit a value. This is useful for cases where you want to react to the latest values from multiple flows.
- The
flattenMerge
operator collects from multiple flows concurrently and merges their emissions into a single flow. This is useful when you want to start collecting from multiple flows simultaneously without waiting for one to complete before starting the next.
- The
merge
operator combines multiple flows by interleaving their emissions without pairing them. It collects from each flow as they emit and emits each value in the order it’s produced.
flatMapConcat
: Concatenates flows sequentially, waiting for each inner flow to complete before moving to the next.
flatMapMerge
: Collects from multiple flows concurrently, merging their emissions as they come.
flatMapLatest
: Cancels the previous flow whenever a new flow is emitted, only collecting the latest emitted flow.
13. What are the different ways to handle exception in flows?
- The
catch
operator is the primary way to handle exceptions in flows. It catches exceptions thrown by the upstream flow and allows you to handle or emit alternative values. - The
onCompletion
operator is a terminal operation that is triggered when the flow completes, either normally or exceptionally. It allows you to perform cleanup actions or log when a flow has finished, regardless of whether it completed successfully or due to an exception.
- We can handle exceptions more granularly by using
emitCatching
(a function you implement to wrapemit
). This approach allows us to catch exceptions within specific parts of the flow and handle them without breaking the flow.
14. How does the retry
operator work with Flow?
The retry
operator allows you to retry the flow when an exception occurs, making it useful for transient errors, such as network issues. You can specify the number of retry attempts and use a predicate to determine which exceptions should trigger a retry.
15. How do you implement a debounce mechanism for user input using flows?
16. Difference between LiveData
& Flows
.
- LiveData is a Hot stream. It starts emitting data as soon as it has an active observer (typically a lifecycle-aware component like an
Activity
orFragment
) and continues to emit values even if there are no observers. Flow is a Cold stream. It only starts emitting data when it’s collected. Each time aFlow
is collected, it starts from the beginning and behaves as if it’s “restarted.” - LiveData is Lifecycle-aware by default. It automatically starts and stops observing based on the lifecycle of the UI component. Flow is not lifecycle-aware by default. When using
Flow
in Android, you must manually manage the lifecycle (e.g., usinglifecycleScope
orrepeatOnLifecycle
). - LiveData is designed for use in the UI layer, especially for observing data in
ViewModel
s. It is tightly integrated with the Android lifecycle, making it ideal for UI-bound data. Flow is a general-purpose reactive data stream that can be used throughout the application, not just in the UI layer. It’s well-suited for managing data streams in repositories, data sources, or any asynchronous data-handling logic. - LiveData doesn’t have built-in error handling. Flow supports built-in error handling operators like
catch
,retry
, andretryWhen
. - LiveData always observes on the main thread, so you don’t need to worry about threading when observing from UI components. Flow allows explicit control over threading using the
flowOn
operator, which lets you specify which dispatcher should be used for upstream operations. - LiveData doesn’t support back pressure handling natively. If data is produced faster than it’s consumed, it could lead to missed updates or performance issues.Flow has built-in back pressure handling, allowing you to use operators like
buffer
,conflate
,collectLatest
, anddebounce
to control the rate of data flow and avoid overwhelming the consumer. - LiveData requires additional handling for one-time events like navigation or showing a message. Patterns like
SingleLiveEvent
orEventWrapper
are commonly used to avoid issues with events being re-emitted on configuration changes. Flow is more suitable for one-time events, especially withSharedFlow
orStateFlow
, which allow you to configure replay behavior and provide finer control over event emission and collection. - LiveData to Flow: You can convert
LiveData
toFlow
using the.asFlow()
extension function. Flow to LiveData: You can convertFlow
toLiveData
using the.asLiveData()
extension function, making it easy to useFlow
in lifecycle-aware contexts.
17. Difference between Flows
& Channels
?
Flow
is a cold stream (starts emitting on collection), whileChannel
is a hot stream (emits values immediately upon being sent).Flow
has a single producer-consumer model, whileChannel
supports multiple producers and consumers, making it ideal for communication between coroutines.Flow
starts and stops with each collection;Channel
can remain open and active until explicitly closed.Flow
has built-in suspension to handle backpressure;Channel
uses buffering to manage backpressure (e.g.,Buffered
,Conflated
).Flow
has built-in error handling with operators likecatch
;Channel
doesn’t support built-in error handling but can propagate exceptions.Flow
offers a rich set of operators (map
,filter
,combine
), whileChannel
provides basicsend
andreceive
methods without transformations.Channel
supports concurrent producers and consumers, whereasFlow
is more suited for sequential processing.Flow
can only be collected once per collector, while aChannel
allows values to be received by multiple consumers.Flow
is ideal for data streams, transformations, and UI updates;Channel
is suited for message passing and producer-consumer patterns.
18. Example of unit testing with flows
An example of a UserViewModel
that fetches a list of users from a repository. This repository exposes a Flow
of user data.
- Step 1: Define the
ViewModel
Step 2: Set Up Test Dependencies
Step 3: Test the UserViewModel
using Flow
and Turbine
.