Search code examples
androidkotlinkotlin-coroutines

Channel vs Shared Flow what is the difference


I read multiple articles trying to find an answer explaining difference.

  1. Quoting the answer here : channel or mutablesharedflow , which one is a better replacement for deprecated localbroadcastmanager

From Roman Elizarov, Channels were added as an inter-coroutine communication primitive.

So they introduced Flow. But Flow is a cold observable, where every subscriber gets their own data (independent from other subscribers). With SharedFlow you get a hot observable that emits independently of having any subscriber(s). You could do the same with ConflatedBroadcastChannel. But JetBrains recommend to use Flow in favor of Channels because of their simpler API.

So if you want to migrate to Coroutines and you need a hot observable multiple subscribers can listen to, you should go with SharedFlow.

  1. https://betterprogramming.pub/stop-calling-kotlin-flows-hot-and-cold-48e87708d863
  • When you want to encapsulate your value-producing code so that consumers don’t have to worry about when it starts, stops or fails, use a flow.

  • when you want to pass values from one coroutine to another, use a channel.

Can you elaborate on those examples? I kinda do not understand when you want to encapsulate your value-producing code so that consumers don’t have to worry about when it starts, stops or fails, use a flow.

Also in the project I work in we are using Channel in MVI View Model implementation.

private val uiEvents = Channel<UiEvent>

(Channel.UNLIMITED)viewModelScope.launch(dispatcher) {
    uiEvents.consumeAsFlow().collect { uiEvent ->

fun push(event: UiEvent) {
    uiEvents.trySend(event)
}

Is that correct use case for Channel?

What meaning is behind "when you want to pass values from one coroutine to another, use a channel"
Can I understand this that way that In one place I for eg send event of type Unit to display a toast in a fragment then? Suppose only one fragment collects this event. I think this could work with Channel and therefore I am confused.


Solution

  • It all hinges upon the overall architecture of your app and the semantics you require.

    Before going into more detail I want to point out one key difference at the start: Channels have a queue semantic, SharedFlows hava a broadcast semantic. That means that an element in a Channel can only be retrieved by one receiver, other receivers will get the next element in the queue and the one after that and so on. A SharedFlow, however, shares its values with anyone who is interested: Every subscriber receives the same values, they are broadcast for everyone to receive.

    When strictly adhering to Unidirectional Data Flow (UDF) Flows are a natural tool to use to propagate data from the lower levels up to the UI and you will rarely find a use case for Channels. Events will only be passed down the layers and not passed around.

    The uiEvents from your example, on the other hand, seems to be more like an event queue. And for such a construct a Channel is well suited. A Channel is effectively a blocking queue, meaning you can have the senders and receivers independently from each other using the same queue while the Channel coordinates the shared access.

    That is also at the core of "pass values from one coroutine to another": Coroutines run concurrently. They can share the same state, but when you make that state mutable so that they can effectively communicate with each other (one coroutine changing a variable with the other one reading the changed value), you will run into all sorts of synchronizing issues: When was that value changed so it needs to be read again? How to guarantee the value can only be changed by one coroutine at a time? Using a Channel here makes it safe for a sender to be sure their change won't be instantly overwritten by another concurrent sender (it is queued), while having a clear signalling mechanism for receivers when there is a new value. You also have some additional configuration options like the buffer size and so on.

    The first link in your question refers to an answer that provides another link to a blog entry of Roman Elizarov, the project lead of Kotlin. Here he lays out the historical reasons for Channels: They were a response to coroutines that were introduced to Kotlin, to have a safe way of communication between coroutines. Cold Flows were added later on to allow other, more flexible semantics and eventually the hot SharedFlow arrived. Now, when "New is always better", why use Channels at all? It's mostly about the semantics. A SharedFlow has broadcast semantics, a Channel has queue semantics. When you need a blocking queue you can do that only with a Channel, not a SharedFlow.

    The second link in your question talks a lot about the definition of cold and hot. Let me provide another angle here: From the perspective of a collector of a cold Flow the code that produces values is executed in the current coroutine. From that follows:

    1. Multiple collectors each receive their own values, not the same values. That is because the producing code is executed repeatedly for each collector, in their own coroutine. Consider the following code:

      val list = List(10) { it } // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
      
      val flow1 = flow {
          list.forEach { emit(it) }
      }
      
      val flow2 = flow {
          repeat(10) { emit(list.random()) }
      }
      

      When flow1 is collected multiple times each collector receives the numbers 0 through 9. While they are actually identical to each other, the forEach loop is nonetheless executed separately for each collector. That becomes more clear when we take a look at flow2: Now each collector receives a series of 10 randomly chosen values from 0 to 9. Since each collector will run the repeat loop for themselves, each one is getting different random numbers.

    2. By the same token each Flow collection starts from the beginning. When one collector has collected the first 3 values so far, another collector starting now to collect the same Flow will not get the 4th value, it will start all over and begin with the first value while the initial collector continues entirely unaffected with the 4th value.

    3. No cleanup necessary: The flow only lives as long as the collector is active. When the collector isn't collecting values there is nothing going on, nothing that needs to be stopped or cleaned up.

    Hot SharedFlows on the other hand are actively producing values without needing a collector to run the producing code for them. Again, from a collector's perspective that means:

    1. Just subscribe and unsubscribe as needed, you will get whatever was already produced by the SharedFlow and saved in its internal replay buffer and then also receive any new values once produced. The current coroutine is only needed to wait for new values, the value-producing code is executed somewhere else.
    2. A SharedFlow never completes. Even when there aren't any new values produced any more, it still has its replay buffer to offer for any new collectors and doesn't shut down or something similar. Other than the previous examples for cold Flows where the collection stopped after 10 values, the collection of a hot flow will never stop and suspends indefinitely.
    3. Since SharedFlows never complete, they also don't need to be cleaned up. If they are actually running code at any given time is something that the collector doesn't need to care about. The SharedFlow can see if it is currently collected and can stop producing values when no one is listening. But that's for the flow itself to decide, nothing the collector should concern itself with.

    Flows come with a powerful API allowing various transformations of the content of the Flow and the configuration of the Flow itself even when it is already running. It is also possible to convert a cold Flow to a hot Flow and vice versa. Most of the operations applied to a Flow only affect the upstream, so the collector in the end has no clear picture of what happend in between. That means that a Flow that is presenting itself as a cold Flow to the collector can very well be based on a hot Flow and, for all intents and purposes, can share all its characteristics.

    For the collector this doesn't pose too much of a problem, though, it won't make much of a difference anyways in how the flow should be handled. Just keep in mind that if you collect a cold Flow multiple times, you will repeat at least some of the value-producing code.

    As a rule of thumb a Flow that is naturally cold should only be made hot at the latest possible time, when it is actually needed. In Android apps that will usually be the view model where the flow is converted to a StateFlow, a specially configured SharedFlow that has the semantics of a single observable value (much like a LiveData or MutableState).