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.
-
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
flowOnoperator. -
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.
The behavior of the app showcases how Flow and Channel work:
-
ColdFibonnaciis implemented with aFlowand exposed to the View with aLiveData. Therefore, whenever the view is no longer present, it'll unobserve theLiveDatathat will propagate that cancellation to the Flow. Whenever the View is present, theLiveDatawill start observing theFlowagain, and because it has a cold observable behavior, it will start the sequence from the beginning. -
NeverEndingFibonacciis implemented with aChannelinstead 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 alaunchcoroutine 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. -
UserRepositoryhas 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.
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.ktto 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.ktwhere 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.
-
asynccreates a coroutine that returns a value. -
launchcreates a coroutine meant as to "fire and forget". -
channelFlowis aFlowwith aChannelbuilt 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 aflowChannelin theColdFibonacciProducer.ktfile. We callsendto emit the new calculated Fibonacci number to the flow's observer. -
ConflatedBroadcastChannelre-emits the last value emitted by the Channel to a new consumer that opens a subscription. This is what we use atNeverEndingFibonacciProducer.ktto create the never-ending Fibonacci. The coroutine created bylaunchhas 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. -
liveDataCoroutines 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 exposedLiveDatais withemit. We callemitevery time we get an element from the flow. Notice thatLiveDataonly works when there's a listener on the other end. When there's an observer,LiveDatawill start consuming the flow and the flow will start from the beginning of the sequence. Whenever the View gets destroyed, theLiveDatawon't be observed and will propagate cancellation to the flow too. This functionality is available in theandroidx.lifecycle:lifecycle-livedata-ktxlibrary. -
lifecycleScope.launchWhenXmethods are available inLifecycleOwnerssuch as Activities. For example, inMainActivity.ktwe uselifecycleScope.launchWhenStartedto 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-ktxlibrary. -
We don't use
GlobalScopeinNeverEndingFibonacciProducer.kt, we create a custom scope that we can cancel (great for testing). If you create coroutines withGlobalScopeyou 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 usesupervisorScopeto 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 asupervisorScopevscoroutineScopecomparison in that file. Another thing to notice is that both of these functions suspend and wait for its children coroutines to finish before resuming.
