stateIn operator doesn't update the cached value of StateFlow - android

I have some shared flows which are merged into several flows in different classes and then these flows are merged into a single one. The original shared flows are configured as such:
protected val statusMutable = MutableSharedFlow<Int>()
val status: Flow<Int> = statusMutable.asSharedFlow()
So, the default configuration. In the end I have code like this:
val status
get() = merge(
object1.status, // merged shared flows
object2.status, // merged shared flows
object3.status, // merged shared flows
statusMutable
).stateIn(GlobalScope, SharingStarted.Lazily, initialValue = someInt)
This flow should be infinite hot flow so I'm using GlobalScope (you can also tell me how to approach this better if there's better way but that's not the actual problem).
The actual problem is that values are emitted and collected fine but the resulting StateFlow doesn't update its value property (therefore the replay cache) for some reason. This leads to a situation where a new subscriber doesn't get the latest emitted value on subscription. What can be the cause of this and how can I fix it?

It turns out the problem is that I access this flow through a getter though I just needed to assign it to the property. Such a stupid mistake! What's funny, I gave up on finding the answer myself and posted this question only to notice a mistake after just like 15 minutes.

Related

Retrieving latest value of a Flow which is not a StateFlow

How can I get the latest value of a Flow? I don't have a StateFlow where I need that latest value. This is the condensed scenario:
There is a repository exposing a StateFlow
val repositoryExposedStateFlow: StateFlow<SomeType> = MutableStateFlow(...)
Additionally there are mappers transforming that StateFlow like
val mappedFlow: Flow<SomeOtherType> = repositoryExposedStateFlow.flatMapLatest { ... }
mappedFlow is no StateFlow anymore, but just a Flow. Thus, I cannot get the latest/current value as I can when there's StateFlow.
Anyhow, I need the latest value in that Flow at some point. Since this point is not in a ViewModel, but some Use Case implementation, I cannot simply perform a stateIn and hold the latest value in the ViewModel all the time the ViewModel is alive -- otherwise I had to pass on the value to all Use Cases. Actually, within a Use Case I trigger a network refresh which leads to emitting of new values on the StateFlow and thus on the mappedFlow, too.
In the Use Cases I have CoroutineScopes though. So I came up with
suspend fun <T> Flow<T>.getState(): T {
return coroutineScope {
val result = stateIn(
scope = this
).value
coroutineContext.cancelChildren()
result
}
}
Without using coroutineContext.cancelChildren() the method will never return, because coroutineScope blocks the caller until all child coroutines have finished. As stateIn never finishes, I manually cancel all children.
Apparently this is a bad thing to do.
But how can I solve this problem in a better way? In my perception the problem arises from StateFlow mapping resulting in regular Flow instances.
Yes, all you need is to call first() on the flow. Since it is backed by a StateFlow upstream, the first() call will get the current value of that backing StateFlow, run it through whatever transformations happen from the downstream operators, and return that value.
This effectively gets you the same result as your attempt above.
The downside is that all the downstream operators must be run, so it is potentially expensive.
This is only possible if there is an upstream StateFlow. Otherwise, there is no concept of a latest value for you to be able to retrieve.
I would challenge your need to get the latest value, though. Typically, you collect flows, so you're already working with a current value. Flows are intended for reactive programming.

Collect flow but only any new values, not the currently existing value

Currently struggling with this one, and so far no combination of SharedFlow and StateFlow have worked.
I have a flow that might have already started with a value, or not.
Using that flow I want to collect any new values that are emitted after I start collecting.
At this moment all my attempts have always failed, no matter what I try it always gets the current value as soon as I start collecting.
An example of what I am trying to achieve:
Given a Flow (could be any type, Int is just for simplification)
with the following timeline: value 4 is emitted | value 2 is emitted | value 10 is emitted
I want to be able to do the following:
If I start collecting after value 4 has already been emitted, I want to only receive anything after that, in this case it would collect 2 and 10 once emitted
If I start collecting after value 2 then it would only receive the 10
If I start collecting before 4 then it would receive 4, 2 and 10
Tried SharedFlow and Stateflow, tried with replay = 0 and WhileSubscribed, no combination I could find would do what I am looking for.
The only workaround so far that I found was to locally register the time I start my .collect{ } and compare with the start time of the item I receive in the collect. In this case I have the object I am using has a specific origin time, but this workaround will not work for everything like the example above with Integers.
EDIT: Adding implementation example as requested for SharedFlow
This is tied to a Room database call that returns a Flow<MyObject>
MyFragment.kt
lifecycleScope.launch(Dispatchers.IO) {
viewModel.getMyObjectFlow.shareIn(
viewModel.viewModelScope, // also tried with fragment lifecyclescope
SharingStarted.WhileSubscribed(), // also tried with the other 2 options
replay = 0,
).collect{
...
}
}
You have a misconception of how flows work. They always emit only after you start collecting. They emit on-demand. Let's get this example:
val flow1 = flow {
println("Emitting 1")
emit(1)
delay(10.seconds)
println("Emitting 2")
emit(2)
}
delay(5.seconds)
println("Start collecting")
flow1.collect {
println("Collected: $it")
}
The output is:
Start collecting
Emitting 1
Collected: 1
not:
Emitting 1
Start collecting
Collected: 1
This is because flow starts emitting only after you start collecting it. Otherwise, it would have nowhere to emit.
Of course, there are flows which emit from some kind of a cache, queue or a buffer. For example shared flows do this. In that case it looks like you collect after emitting. But this is not really the case. Technically speaking, it works like this:
val buffer = listOf(1 , 2, 3)
val flow1 = flow {
buffer.forEach {
println("Emitting $it")
emit(it)
}
}
It still emits after you start collecting, but it just emits from the cache. Of course, the item was added to the cache before you started collecting, but this is entirely abstracted from you. You can't know why a flow emitted an item. From the collector perspective it always emitted just now, not in the past. Similarly, you can't know if a webserver read the data from the DB or a cache - this is abstracted from you.
Summing up: it is not possible to collect only new items from just any flow in an universal way. Flows in general don't understand the concept of "new items". They just emit, but you don't know why they do this. Maybe they somehow generate items on-the-fly, maybe they passively observe external events or maybe they re-transmit some items that they collected from another flow. You don't know that.
While developing your solution, you need to understand what was the source of items and develop your code accordingly. For example, if the source is a regular cold flow, then it never starts doing anything before you start collecting. If the source is a state flow, you can just drop the first item. If it is a shared flow or a flow with some replay buffer, then the situation is more complicated.
One possible approach would be to start collecting earlier than we need, initially ignore all collected items and at some point in time start processing them. But this is still far from perfect and it may not work as we expect.
It doesn't make sense to use shareIn at the use site like that. You're creating a shared Flow that cannot be shared because you don't store the reference for other classes to access and use.
Anyway, the problem is that you are creating the SharedFlow at the use site, so your shared flow only begins collecting from upstream when the fragment calls this code. If the upstream flow is cold, then you will be getting the first value emitted by the cold flow.
The SharedFlow should be created in the ViewModel and put in a property so each Fragment can collect from the same instance. You'll want to use SharingStarted.Eagerly to prevent the cold upstream flow from restarting from the beginning when there are new subscribers after a break.

Converting livedata to stateflow

Android, Kotlin
I have the following livedata in my datasource class, I cannot change this to StateFlow, so need to convert it to StateFlow in my viewModel
val trackingCatalogInitialLoadLiveData: LiveData<Pair<CatalogTracking, Int>> by lazy {
instantSearchDataSourceLiveData.switchMap { instantSearchDataSource ->
instantSearchDataSource.initialLoadLiveData
}
}
In My ViewModel I have the following, and this is the part I am not sure about if this is the correct way to convert LiveData to StateFlow:
val trackingCatalogInitialLoadStateFlow: StateFlow<Pair<CatalogTracking, Int>> by lazy {
instantSearchDataSourceFactory.trackingCatalogInitialLoadLiveData.asFlow()
.stateIn(viewModelScope, SharingStarted.Lazily, Pair(CatalogTracking(), 0))
}
Then in my fragment I just collect the results
coroutineScope.launch {
mInstantSearchViewModel.trackingCatalogInitialLoadStateFlow.collect { trackingPair ->
// code here
}
Is this the best practice to convert LiveData to StateFlow? Anything I should be looking out for?
You don't need to use by lazy. asFlow() and stateIn() both create simple wrappers, so they are trivial to call directly in the property initializer.
As #Joffrey said, if you use SharingStarted.Lazily, inspecting the flow's value before it has any collectors will incorrectly show your provided initial value. Since LiveData is hot, starting your StateFlow lazily doesn't buy you a lot. The underlying coroutine that transfers LiveData values to the StateFlow is doing a trivial amount of work.
If you don't need to inspect the value (in most cases you probably don't), then it should be fine to leave it as a cold Flow. Even though the Flow from asFlow() is cold, the underlying LiveData is still hot, so when collectors of the flow collect it, they'll always get the latest value. The main behavior difference would be if your data source does not provide a guaranteed initial value for the LiveData, then a StateFlow gives you the opportunity to emit your provided default initially without waiting for the LiveData to publish its first value.

What are the differences between StateFlow and LiveData?

as I mentioned in the title, I'm curious about the general differences between the two. Can you help with this? I couldn't find the specific differences as there are complex examples on the internet.
What are the differences in terms of performance?
In which scenarios does it provide advantages?
Using StateFlow with Kotlin Flow is advantageous. But what is the risk of not switching to StateFlow in a project using LiveData?
Is Google deprecating LiveData? :)
I just switched to StateFlow, so this is a great time for me to answer your question.
What are the differences in terms of performance?
Honestly, I don't know, but since it's pushed by Kotlin and Android, just trust them :).
In which scenarios does it provide advantages?
For LiveData you are not forced to give an initial value, it may end up writing more code in init{}; But for StateFlow you are Forced to give an initial value (including null), it may save your code a bit.
For LiveData even if you give an initial value, you still need to do Null Check when you access its value (see this), it's kind of annoying. But that's not gonna happen on StateFlow - it will be what it should be.
For LiveData you cannot easily, or elegantly observe data changes JUST inside ViewModel, you are gona use observeForever() which is also mentioned in here. But for StateFlow it's easy, do it like following:
class FirstViewModel() : ViewModel() {
val uiScope = viewModelScope
val name = MutableStateFlow("Sam") //must have initial value
//val name = MutableStateFlow<String?>(null) //null is acceptable
init {
observeName()
}
private fun observeName() = uiScope.launch { //must run in coroutine scope
name.collect { name -> //for Fragment / Activity, use lifecycleScope.launch{}
//do your stuff
}
}
}
Using StateFlow with Kotlin Flow is advantageous. But what is the risk of not switching to StateFlow in a project using LiveData?
What is the risk of not switching to Kotlin in a project using Java? :)
Is Google deprecating LiveData?
I would say yes, and they would say no, no for "not yet to say it loudly" :).

Using mutableStateOf instead of observeAsState

I'm working with Jetpack Compose in an Android app and had the problem that my uiState (LiveData) was set to its initial value on every recomposition, since I've initialized it like
val authUiState: AuthUIState by authenticationViewModel.uiState.observeAsState(AuthUIState.Loading)
It was set to Loading on every recomposition before it was set to the correct value.
When I tried to Remember the value, I learned that we can't use observeAsState within the remember block and finally changed it to
val authUiState = remember{ mutableStateOf(authenticationViewModel.uiState.value) }.value
This works, but I'm not quite sure, if this is the common and good way to solve this.
What do you think? Should I do it differently? Do you need more information?
See if the uiState inside your viewmodel is something like a LiveData Object, (which is kinda what it seems like from the code), the recommended way is to store it in the viewmodel itself as mutable state.
var uiState by mutableStateOf (initialValue)
private set //Do not allow external modifications to maintain consistency of state
fun onUiStateChange(newValue: Any){
uiState = newValue
}
You just need to initialise it as a MutableState, in the rest of the code, to update, delete or whatever you want to do with it, just treat it as a regular variable. Compose will trigger recomposition every time the value is updated.
The following code snippet below will almost certainly not work, because here, the state is whatever you wrap inside mutableStateOf(), which is just a simple value which will be fetched once from the viewmodel and then remembered throughout recompositions, so no code change will be triggered here
val authUiState by remember{ mutableStateOf(authenticationViewModel.uiState.value) }
Storing state in the viewmodel as mutableState, is as far as my knowledge extends, the best practice in compose. You will see the same in the 'State Codelab' from Android developers
Good luck

Categories

Resources