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
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
Flow and Channels in the app
The behavior of the app showcases how
ColdFibonnaciis implemented with a
Flowand exposed to the View with a
LiveData. Therefore, whenever the view is no longer present, it'll unobserve the
LiveDatathat will propagate that cancellation to the Flow. Whenever the View is present, the
LiveDatawill start observing the
Flowagain, and because it has a cold observable behavior, it will start the sequence from the beginning.
NeverEndingFibonacciis implemented with a
Channelinstead of a
Flow. 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 a
launchcoroutine 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 with
NeverEndingFibonacci, this functionality is agnostic of View lifecycle events and has its own lifetime and that's why it's also implemented with a
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 the
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".
Channelbuilt 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 a
Channel(being able to send elements between coroutines). We defined a
ColdFibonacciProducer.ktfile. We call
sendto 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 at
NeverEndingFibonacciProducer.ktto create the never-ending Fibonacci. The coroutine created by
launchhas 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 in
MainViewModel.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 exposed
emit. We call
emitevery time we get an element from the flow. Notice that
LiveDataonly 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, the
LiveDatawon't be observed and will propagate cancellation to the flow too. This functionality is available in the
lifecycleScope.launchWhenXmethods are available in
LifecycleOwnerssuch as Activities. For example, in
lifecycleScope.launchWhenStartedto create a coroutine that will get executed when the LifecycleOwner is at least
Started. Inside that coroutine, we can consume the elements from the ColdFibonacci flow. This functionality is available in the
We don't use
NeverEndingFibonacciProducer.kt, we create a custom scope that we can cancel (great for testing). If you create coroutines with
GlobalScopeyou manually have to track down every coroutine you create, whereas with a custom scope you can track them all together.
coroutineScope. You can find this in
UserRepository.kt. If you notice,
getUserAsync()is a suspend function; we use
supervisorScopeto 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 a
coroutineScopecomparison in that file. Another thing to notice is that both of these functions suspend and wait for its children coroutines to finish before resuming.