Flow emits different values when collecting it multiple times - android

I created a Flow from which I emit data. When I collect this flow twice, there are 2 different sets of data emitted from the same variable instead of emitting the same values to both collectors.
I have a simple Flow that I created myself. The text will be logged twice a second
val demoFlow: Flow<String> = flow {
while (true) {
val text = "Flow ${(0..100).random()}"
Log.d("TAG", text)
emit(text)
delay(1000)
}
}
In my viewModel I have a simple function that gets the previous Flow
fun getSimpleFlow() = FlowRepository.demoFlow
And in my Fragment I collect and display my Flow
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.getSimpleFlow().collect {
binding.tv1.text = it
}
}
launch {
viewModel.getSimpleFlow().collect {
binding.tv2.text = it
}
}
}
}
If I transform the Flow to a StateFlow or a SharedFlow, I no longer have this problem.
I don't understand how or why this happens since I'm using the same 'demoFlow' variable.
Is there a way to get the same values from 'demoFlow' without converting to a StateFlow or a SharedFlow?

Regular Flows are cold, this behaviour is by design.
The demoFlow is the same, so you have the same Flow instance. However, collecting the flow multiple times actually runs the body inside the flow { ... } definition every time from the start. Each independent collection has its own variable i etc.
Using a StateFlow or a SharedFlow allows to share the source of the flow between multiple collectors. If you use shareIn or stateIn on some source flow, that source flow is only collected once, and the items collected from this source flow are shared and sent to every collector of the resulting state/shared flow. This is why it behaves differently.
In short, reusing a Flow instance is not sufficient to share the collection. You need to use flow types that are specifically designed for this.

Related

why mutableSharedFlow subscriptionCount changes when use ShareIn

why it changes whether use ShareIn func or not
val sharedf = MutableSharedFlow<Int>()
fun Flow<Int>.print() : Flow<Int> {
return map {
println("in print : $it")
it
}
}
fun Flow<String>.printS() : Flow<Int> {
return map {
println("in print : $it")
it.toInt()
}
}
fun Flow<Int>.toNext() : Flow<Int> {
return merge(
filterIsInstance<Int>().print(),
filterIsInstance<String>().printS(),
)
}
when I use main func like this
fun main() = runBlocking<Unit> {
launch {
sharedf.onSubscription{
println("subsCount : ${sharedf.subscriptionCount.value}")
}
.shareIn(GlobalScope, SharingStarted.Lazily)
.toNext()
.collect()
}
launch {
for (i in 0..3){
delay(500)
sharedf.emit(i)
}
}
}
subsCount : 1
but if I remove ShareIn
subsCount : 2
why I have to Use ShareIn even sharedf is already MutableSharedFlow
Looking at toNext, you can see that it builds 2 flows from the receiver (this) flow, by using twice filterIsInstance, and merging those 2 flows. Therefore, when you collect someFlow.toNext(), it will collect twice the original source flow someFlow.
This is why, if you don't put shareIn in your main method, you see 2 subscribers on sharedf. The toNext().collect() subscribes twice to the shared flow due to the implementation of toNext() as we've just seen.
Now, if you add shareIn() in the middle, you create another, independent shared flow. Let's call it shared2, and extract it to a variable to make it clearer:
val shared2 = sharedf.onSubscription { ... }.shareIn(GlobalScope, SharingStarted.Lazily)
shared2.toNext().collect()
In a way, that new shared flow shared2 "protects" the source flow sharedf from collectors. When many collectors collect shared2, shared2 only collects once the source flow sharedf.
So, even if toNext() collects shared2 twice, shared2 only collects sharedf once, which is why you only see 1 subscription on sharedf in your log.
why I have to Use ShareIn even sharedf is already MutableSharedFlow
What makes you think you have to do that? Creating a shared flow via shareIn out of a flow that's already a shared flow is indeed unexpected.
Side note: you can use onEach instead of map in your print function
The way shareIn works in very simple terms is that it launches a coroutine that runs indefinitely and collects the source flow to re-emit those values. So unlike the map and merge operators that create cold Flows that only subscribe to the source when they're collected, shareIn creates a Flow that immediately starts collecting from the source Flow, so it is immediately counted as a subscriber of the source flow.
The downstream toNext() wraps the second SharedFlow, not the first. The first one doesn't know about downstream subscribers. It is only emitting to the single SharedFlow from the shareIn call, so it has only one subscriber.
When you remove shareIn, then your top level SharedFlow is emitting to both of the subscribers that are created in your toNext()'s merge() call, so it sees two subscribers. Note that it will not see any subscribers until collect is called, since those are cold wrappers.

How to know when job from viewModel is done

I am trying to figure out how jobs with coroutines work. Basically, I want to launch this coroutine from FirstFragment and after that navigate to SecondFragment and get notified when this job is done. I call getData() in FirstFragment onViewCreated() and navigate to SecondFragment. Whether I write getData().isCompleted or getData().invokeOnCompletion { } in SecondFragment nothing happens. I don't know if I am missing something or not starting job correctly or something else.
private val _data = MutableStateFlow<GetResource<String>?>(null)
val data: StateFlow<GetResource<String>?> = _data
fun getData() = viewModelScope.launch {
repository.getData().collect {
_data.value = it
}
}
A Flow from a database never completes because it is supposed to monitor the database for changes indefinitely. It only stops when the coroutine is cancelled. Therefore the Job that collects such a Flow will never complete. Also, if you call getData() on the repo again, you are getting a new Flow instance each time.
Regardless of what you're doing, you need to be sure you are using the same ViewModel instance between both fragments by scoping it to the Activity. (Use by activityViewModels() for example.) This is so the viewModelScope won't be cancelled during the transition between Fragments.
If all you need is a single item from the repo one time, probably the simplest thing to do would be to expose a suspend function from the repo instead of a Flow. Then turn it into a Deferred. Maybe by making it a Lazy, you can selectively decide when to start retrieving the value. Omit the lazy if you just want to start retrieving the value immediately when the first Fragment starts.
// In the shared view model:
val data: Deferred<GetResource<String>> by lazy {
viewModelScope.async {
repository.getData() // suspend function returning GetResource<String>
}
}
fun startDataRetrieval() { data } // access the lazy property to start its coroutine
// In second fragment:
lifecycleScope.launch {
val value = mySharedViewModel.data.await()
// do something with value
}
But if you have to have the Flow because you’re using it for other purposes:
If you just want the first available value from the Flow, have the second Fragment monitor your data StateFlow for its first valid value.
lifecycleScope.launch {
val value = mySharedViewModel.data.filterNotNull().first()
// do something with first arrived value
}
And you can use SharedFlow so you don’t have to make the data type nullable. If you do this you can omit filterNotNull() above. In your ViewModel, it’s easier to do this with shareIn than your code that has to use a backing property and manually collect the source.
val data: SharedFlow<GetResource<String>> = repository.getData()
.shareIn(viewModelScope, replay = 1, SharingStarted.Eagerly)
If you need to wait before starting the collection to the SharedFlow, then you could make the property lazy.
Agreed with #Tenfour04 's answer, I would like to contribute a little more.
If you really want to control over the jobs or Structured Concurrency, i would suggest use custom way of handling the coroutine rather than coupled your code with the viewModelScope.
There are couple of things you need to make sure:
1- What happen when cancellation or exception occurrs
2- you have to manage the lifecycle of the coroutine(CoroutineScope)
3- Cancelling scope, depends on usecase like problem facing you are right now
4- Scope of ViewModel e.g: Either it is tied to activity(Shared ViewModel) or for specific fragment.
If you are not handling either of these carefully specifically first 3, your are more likely to leaking the coroutine your are gurenteed gonna get misbehavior of you app.
Whenever you start any coroutine in Custom way you have to make sure, what is going to be the lifecycle, when it gonna end, This is so important, it can cause real problems
Luckily, i have this sample of CustomViewModel using Jobs: Structured Concurrency sample code

Collecting from Flow in UI with repeatOnLifeCycle

I started to replace LiveData with Flow since it is more flexible. But then I find out you need to write enormous amount of boilerplate code to observe from Flow in UI.
In the StateFlow documentation, it says that
LiveData.observe() automatically unregisters the consumer when the view goes to the STOPPED state, whereas collecting from a StateFlow or any other flow does not stop collecting automatically. To achieve the same behavior,you need to collect the flow from a Lifecycle.repeatOnLifecycle block.
It's also mentioned in the article by Manuel Vivo that using collecting from lifecycleScope.launchWhenX is dangerous and should not be used in UI because the producer flow will not stop emitting.
He recommended us to use
// Listen to multiple flows
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// As collect is a suspend function, if you want to collect
// multiple flows in parallel, you need to do so in
// different coroutines
launch {
flow1.collect { /* Do something */ }
}
launch {
flow2.collect { /* Do something */ }
}
}
}
The amount of boilerplate code is too much. Is it not possible to do it in a two liner like what LiveData does?
viewModel.movieData.observe(viewLifecycleOwner) {
...
}
Why is it so complex to collect from Flow in UI? Is it advisable to convert the Flow to LiveData with asLiveData()?
You could build extensions to reduce the boilerplate
inline fun <T> Flow<T>.collectIn(
owner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
crossinline action: suspend CoroutineScope.(T) -> Unit
) = owner.addRepeatingJob(minActiveState, coroutineContext) {
collect {
action(it)
}
}
This makes collecting flows similar to LiveData as
flow.collectIn(viewLifecycleOwner){ /* do stuff */ }
First answer your first question: Flow is a cold flow. And Flow is stateless. If you provide Flow, then it means that you need to construct and collect Flow frequently.
In another case, if Hot Flow is provided, such as (StateFlow), although the hot flow provides state (.value), it does not know anything about the life cycle of Android. As you said, you can use launchWhenXXX() to collect Flow.
When using launchWhenXXX(), you must pay attention to the life cycle of the hot flow. When to start collect and when to end collect, these need to be paid attention to. So it seems very troublesome. Of course, Flow is a way to get rid of using LiveData.
For details, please refer to: https://proandroiddev.com/should-we-choose-kotlins-stateflow-or-sharedflow-to-substitute-for-android-s-livedata-2d69f2bd6fa5
The second question: LiveData manages the life cycle of Android. Flow.asLiveData() is completely desirable. At this time, only a simple Observe is needed.

Migrating a MediatorLiveData to SharedFlow

I have a MediatorLiveData that uses three LiveData sources. When any of them emits a new value and I have at least one of each, I use the three values to produce the output for the UI.
Two of the sources are user settings for how to sort and filter a list, and the third is the list data, pulled from a Room database Flow.
It looks something like this:
val thingsLiveData: LiveData<List<Thing>> = object: MediatorLiveData<List<Thing>>() {
var isSettingA: Boolean = true
var settingB: MySortingEnum = MySortingEnum.Alphabetical
var data: List<Thing>? = null
init {
addSource(myRepo.thingsFlow.asLiveData()) {
data = it
dataToValue()
}
addSource(settingALiveData) {
isSettingA= it
dataToValue()
}
addSource(settingBLiveData) {
settingB= it
dataToValue()
}
}
private fun dataToValue() {
data?.let { data ->
viewModelScope.launch {
val uiList = withContext(Dispatchers.Default) {
produceUiList(data, isSettingA, settingB)
}
value = listItems
}
}
}
}
I'm looking for a clean way to convert this to a SharedFlow, preferably without any #ExperimentalCoroutinesApi. The only SharedFlow builder function I've come across is callbackFlow, which isn't applicable. Are you intended to use flow { ... }.asSharedFlow(...) in most cases, and if so, what would that look like here?
The two settings LiveData I also plan to migrate to flows.
The source Flows can be combined using combine(), which creates a cold Flow that, when collected, will start collecting from its source Flows, which may be hot or cold.
I was originally thinking that I must be missing something and that there should be some way to directly combine hot Flows into a combined hot Flow. But I realized it makes sense that the operators should only return cold Flows and leave it up to you to convert it back to a hot Flow if that's what you need.
In many cases, such as mine, it's perfectly fine to leave it cold. I only collect this Flow from one place in my UI, so it doesn't matter that it only starts combining the sources when it's collected. The source hot Flows don't care whether something is currently collecting them or not...they just keep emitting regardless.
If I collected this Flow from multiple places or multiple times, then it might make sense to use shareIn on the combined Flow to make it hot, which would avoid redundant work of combining the sources. The potential downside would be that it would combine those sources even when nothing is collecting, which would be wasted work.
val thingsFlow: Flow<List<Thing>> = combine(
myRepo.thingsFlow,
settingALiveData.asFlow(),
settingBLiveData.asFlow()
) { data, isSettingA, settingB -> produceUiList(data, isSettingA, settingB) }
// where produceUiList is now a suspend function that wraps
// blocking code using withContext

Kotlin flow (or something similar) that can be collected with multiple collectors

I tried using Kotlin Flow to be some kind of message container which should pass this message to all observers (collectors). I do not want to use LiveData on purpose because it need to be aware of lifecycle.
Unfortunately I have noticed that if one collector collects message from flow no one else can receive it.
What could I use to achieve "one input - many output".
You can use StateFlow or SharedFlow, they are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers.
From the documentation, available here:
StateFlow: is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property.
SharedFlow: a hot flow that emits values to all consumers that collect from it. A SharedFlow is a highly-configurable generalization of StateFlow.
A simple example using state flow with view model:
class myViewModel() : ViewModel() {
val messageStateFlow = MutableStateFlow("My inicial awesome message")
}
You can emit a new value using some scope:
yourScope.launch {
messageStateFlow.emit("My new awesome message")
}
You can collect a value using some scope:
yourScope.launch {
messageStateFlow.collect {
// do something with your message
}
}
Attention: Never collect a flow from the UI directly from launch or the launchIn extension function to update UI. These functions process events even when the view is not visible. You can use repeatOnLifecycle as the documentation sugests.
You can try BehaviorSubject from rxJava. Is more comfortable to use than poor kotlin.flow. Seems like this link is for you: BehaviorSubject vs PublishSubject
val behaviorSubject = BehaviorSubject.create<MyObject> {
// for example you can emit new item with it.onNext(),
// finish with error like it.onError() or just finish with it.onComplete()
somethingToEmit()
}
behaviorSubject.subscribe {
somethingToHandle()
}

Categories

Resources