Android kotlin flows collects initial values when visible

Viewed 35

I was trying out different kinds of flows like flows with channel, sharedflows and stateflows. What I did was, suppose I have a MainActivity, inside it I have two buttons side by side at the top and below them a fragmentContainerView. Initially the fragmentContainerView doesn't have any fragment.

Now I have a viewModel where I am emitting a range of int values in a loop with 1 or 2 seconds delay with all three flow types. And I have consumers of the values in MainActivity, fragmentA and fragmentB (fragmentB has collectLatest in all three flows when collecting). Clicking button1 attaches fragmentA and Button2 attaches fragmentB.

Now what happens after the values are started emitting suppose initially from 0. The mainActivity starts receiving as soon as the values are emitted. Then when I click button1, fragmentA starts receiving from initial value 0. After sometime I click button2 which removes fragmentA and attaches fragmentB, now fragmentB starts receing from value 0 which has collectLatest. Again if I click button1, fragmentA starts receiving from initial value 0.

I can understand that when the fragments are not visible they should not receive any values. But I want to understand is this the intended behaviour like whenever a new fragment is coming visible its receiving from initial value instead of having collectLatest which did not work. Am I doing anything wrong or why is it happening like this? Are the previous initial values stored in some form of cache? and if I somewhere want to get the current latest value when the view is visible, in what way can I do it? Guidance with some sample code will help. Thank you

1 Answers

Sounds like you are using cold flows instead of hot flows.

The behavior of cold flows is that each new collector gets values starting from the very beginning (the flow producer starts a new production process for each collector). For example, if you use the flow Flow builder, like this:

val flow = flow {
    for (i in 1..3) {
        emit(i)
        delay(100)
    }
}

Then each time a coroutine calls collect on it, that coroutine will get a fresh new stream of values, starting from the beginning of the above lambda function.

With a hot flow, the behavior depends on the implementation. Channel-based flows fan out, which means no two collectors will ever receive the same value. For each value emitted, only one collector will receive it. SharedFlows can have a buffer that replays up to a certain number of past values for every collector. A StateFlow behaves like a SharedFlow with replay value of 1. Each new collector can only collect the most recent value followed by any further latest values, and if it is slower at collecting than values are being produced, it will skip values.

The generally recommended type of flow to use in a ViewModel that fits most uses is a SharedFlow with a replay buffer of 1, and if based on an upstream flow using shareIn, a SharingStarted of WhileSubscribed(5000). This is a hot flow, but new subscribers get the most recently emitted value from the replay. So if the screen is rotated, the most recent value is still in memory and can be immediately displayed in the UI. The SharingStarted.WhileSubscribed(5000) allows it to stop collecting from the upstream flow when there are no more views on screen collecting from it, but the 5 second buffer waits to make sure it's not just a screen rotation causing a very temporary lack of subscribers.

Related