This sample showcases an Android app that uses both Flow and Channel from Kotlin Coroutines.
It also includes tests! Very important to have a maintainable application.
Differences between Flow and Channel
-
Flow has a cold behavior, every time an observer applies a terminal operator on it, it'll start executing the code from the beginning. Channels are hot and they run even if there are no observers listening for events. With a regular Channel, only one observer will get the element emitted from the Channel. With a BroadcastChannel, all observers get the same element emitted, it broadcasts the emission of the element.
-
Use Channels when the producer and the consumer have different lifetimes. For example, a View and a ViewModel have different lifetimes, you may not want to consume a Flow from the View because it'll start execution every time that the View gets created (for instance) and there's no way to continue execution or get the last emitted value. For that, use Channels. Not maintaining state is really bad for configuration changes.
-
Normally, when creating a Channel, you specify the Dispatcher it'll execute its code on. However, this is not true when creating a Flow. In a Flow, you don't specify the dispatcher because it will be executed in the consumer's dispatcher by default. In case you want to modify it, you have the
flowOn
operator. -
Channels should be an implementation detail in your app. Even if you need to create one because producer and consumer have different lifetimes, NEVER expose a Channel, expose a Flow instead. You can use the
asFlow()
operator.
Flow and Channels in the app
The behavior of the app showcases how Flow
and Channel
work:
-
ColdFibonnaci
is implemented with aFlow
and exposed to the View with aLiveData
. Therefore, whenever the view is no longer present, it'll unobserve theLiveData
that will propagate that cancellation to the Flow. Whenever the View is present, theLiveData
will start observing theFlow
again, and because it has a cold observable behavior, it will start the sequence from the beginning. -
NeverEndingFibonacci
is implemented with aChannel
instead of aFlow
. Since Channels are hot, it'll keep emitting Fibonacci numbers even if there are no consumers listening for the events. We create the loop to emit items within alaunch
coroutine because we just want to start and forget about it, we don't want to return anything, we'll send the elements to the Channel. The View will consume/subscribe to this Channel by means of the Flow interface. When listening for number updates, if it unsubscribes from the Flow, it's ok, nothing happens, it'll keep producing numbers. Whenever it collects again (maybe after a configuration change) from the Flow, the consumer will receive the last item emitted to the Channel and the new ones as they're produced. -
UserRepository
has the use case of returning a deferred computation. However, although it's not fully implemented, it has the logic of how you could expose a stream of User objects. Imagine that you want to handle user sessions and want to expose to the rest of the application the User that is logged in at any point. As withNeverEndingFibonacci
, this functionality is agnostic of View lifecycle events and has its own lifetime and that's why it's also implemented with aConflatedBroadcastChannel
.
Launching coroutines
There are two clearly-defined ways to create coroutines:
-
Launch: This is "fire and forget" kind of coroutine. It doesn't return any value. E.g. a coroutine that logs something to console. We use this in
ColdFibonacciProducer.kt
to start our Fibonacci computation, here we don't need to return a value since we're sending the numbers to theChannel
. -
Async: creates a Coroutine that returns a value. E.g. a coroutine that returns the response of a network request. We use it in
UserRepository.kt
where we create a coroutine to obtain the user information. Why we create a coroutine? Retrieving that information can be expensive and we might want to do it on a background thread.
Learnings
-
async
creates a coroutine that returns a value. -
launch
creates a coroutine meant as to "fire and forget". -
channelFlow
is aFlow
with aChannel
built in. This gives you the behavior of a cold stream (starts the block of code every time there's an observer) with the flexibility of aChannel
(being able to send elements between coroutines). We defined aflowChannel
in theColdFibonacciProducer.kt
file. We callsend
to emit the new calculated Fibonacci number to the flow's observer. -
ConflatedBroadcastChannel
re-emits the last value emitted by the Channel to a new consumer that opens a subscription. This is what we use atNeverEndingFibonacciProducer.kt
to create the never-ending Fibonacci. The coroutine created bylaunch
has its own scope so until you don't cancel it's job, it'll continue producing numbers. All the numbers produced are sent to the Channel that can be consumed from the outside. Whenever there's a new subscription, the observer will get the last item emitted by the channel plus the new ones. -
liveData
Coroutines builder. You can find the code inMainViewModel.kt
. This builder creates a new coroutine (with its own scope) so that now you can call suspend functions inside the builder. In the builder, we call collect on the flow to consume the elements. The way we emit to the exposedLiveData
is withemit
. We callemit
every time we get an element from the flow. Notice thatLiveData
only works when there's a listener on the other end. When there's an observer,LiveData
will start consuming the flow and the flow will start from the beginning of the sequence. Whenever the View gets destroyed, theLiveData
won't be observed and will propagate cancellation to the flow too. This functionality is available in theandroidx.lifecycle:lifecycle-livedata-ktx
library. -
lifecycleScope.launchWhenX
methods are available inLifecycleOwners
such as Activities. For example, inMainActivity.kt
we uselifecycleScope.launchWhenStarted
to create a coroutine that will get executed when the LifecycleOwner is at leastStarted
. Inside that coroutine, we can consume the elements from the ColdFibonacci flow. This functionality is available in theandroidx.lifecycle:lifecycle-runtime-ktx
library. -
We don't use
GlobalScope
inNeverEndingFibonacciProducer.kt
, we create a custom scope that we can cancel (great for testing). If you create coroutines withGlobalScope
you manually have to track down every coroutine you create, whereas with a custom scope you can track them all together. -
supervisorScope
&coroutineScope
. You can find this inUserRepository.kt
. If you notice,getUserAsync()
is a suspend function; we usesupervisorScope
to create a new scope out of the one that is calling the method. And this is because we need a scope to create new coroutines! Find asupervisorScope
vscoroutineScope
comparison in that file. Another thing to notice is that both of these functions suspend and wait for its children coroutines to finish before resuming.