Hello, when using ```val viewModel: SomeViewModel ...
# compose-android
g
Hello, when using
Copy code
val viewModel: SomeViewModel = hiltViewModel()
val someUiState by viewModel.uiState.collectAsStateWithLifecycle()
And having an uiState defined as the following (in a ViewModel)
Copy code
private val _uiState by lazy { MutableStateFlow(initializeUiState()) }
    override val uiState get() = _uiState.asStateFlow().apply {
        onStart { Timber.d("onStart uiState on = $this")}
        onCompletion { Timber.d("onCompletion uiState on = $this")}
    }
onStart
and
onCompletion
are not called, what am I missing here?
s
onStart and onCompletion return a new Flow which you’d need to turn hot for these to take effect. As I see it, you take the StateFlow, and run apply on it, so the call site just takes that StateFlow as it is. And inside the apply, you call onStart and onCompletion, which both return a new flow built on top of the StateFlow you got, but you’re not doing anything with the Flow<T> that those two functions return, as if you never called those functions.
g
You're right 😮
Is there any other way to know that a StateFlow has observers and when it doesn't anymore?
s
Why do you need to know that? Just curious.
g
I'd like to stop listening to some hot flows up the stack when the screen is on paused, aka the uiState is not listened to
s
Aha, so you’re looking for
stateIn()
here
g
ah!
s
Something like this. Make a cold flow which specifies the work, and turn it hot through stateIn + SharingStarted._WhileSubscribed_(5.seconds). This will turn off the flow again after 5 seconds have passed when there’s nobody listening to it. Or adjust the 5 seconds according to your needs
g
Looks promising, testing now!
🦠 1
Can my source still be a MutableStateFlow in addition to that? I guess so
s
Yeah it can be, but that will be hot forever. If it’s only storing a value then I don’t think it’d be a problem.
g
Yeah, I still wanna keep the MutableStateFlow but be able to know if there are observers to stop listening to hot flows up in the stack
s
Here for example we got a combination of a hot MutableStateFlow (which only holds a value, so no problem, and a cold flow (inputResult) and that goes hot only in the presence of observers.
g
Thanks, looking into all of this 😄
@Stylianos Gakis What do you think about this way?
Copy code
MutableStateFlow(initializeUiState()).apply {
            subscriptionCount
                .map { count -> count > 0 }
                .distinctUntilChanged()
                .onEach { isActive ->
                    Timber.d("ViewModel ${this@MVIViewModel} has active observers on UI State = $isActive")
                    if (isActive) onUiStateHasObservers() else onUiStateHasNoObservers()
                }
                .launchIn(viewModelScope)
        }
s
I really wonder what you want to do inside
onUiStateHasObservers
and
onUiStateHasNoObservers
g
Cancel a job that collects a hot flow on
onUiStateHasNoObservers
and create it
onUiStateHasObservers
s
But, if you just make that job be inside the flow before the
stateIn
you get this for free, it cancels itself on no observers. Just like in the places I linked you to.
g
I can't do it in the same place, as in one place I'm in a ViewModel, and up the stack I have that hot flow coming from Room from something that is a Singleton itself (it syncs data into room from a 3rd party socket)
But I'll check again
stateIn
, maybe I can come up with something bettter
s
Does room give you hot flows, not cold ones?
Or wait, your own thing which uses Room is hot
g
In that Singleton, I wanna know how much observers there are on the Room Flow, to react to it
s
Well, does that need to be hot then? If in the end all you do is turn it cold again when there’s no observers? Maybe that can also just return
Flow
instead of
StateFlow
and make the entire chain cold until you have an observer with
stateIn(WhileSubscribed)
Does the number of observers play any significant role?
(Aside from if there is at least 1/there are none)
g
Yeah, the fact that the hot flow from Room is observed, I then need to set an info on the observed Row. As this flow is observed in the scope of the ViewModel, nothing stops the ViewModel from continuing to observe the hot flow when the UiState has no observers, so the idea here is to be notified of that (isActive) and manage the job consequentially This issue here is that an observed hot flow in ViewModel that updates a MutableStateFlow can't know itself that the MutableStateFlow is not observed anymore, hence this thread 😛
s
You’re losing me a bit here tbh 😄 Could you show to me what exactly you mean by
the hot flow from Room is observed
? What is a hot from from Room?
g
Sorry 😛 I mean that I collect a flow from a query from Room which is hot because updated by changes on the table:
Copy code
job = viewModelScope.launch {
    client.observeSomething(somethingId, BULK_SIZE)?.collect { something ->
        update { state ->
            state.copy(
                id = something?.id ?: "",
            )
         }
     }
}
The end result looks like this:
Copy code
private fun MutableStateFlow<UiState>.notifyIsActive() = apply {
        subscriptionCount
            .map { count -> count > 0 }
            .distinctUntilChanged()
            .dropWhile { false }
            .onEach { isActive -> if (isActive) onUiStateActive() else onUiStateNotActive() }
            .launchIn(viewModelScope)
    }

    protected open fun onUiStateActive() {}
    protected open fun onUiStateNotActive() {}
s
So it’s not really a hot from from Room, you are turning it hot inside your VM, and storing its job in a local variable. Why could this whole thing not be cold by nature if there are no observers by doing something more like
Copy code
val uiState: StateFlow<UiState> = flow {
  client... do stuff in here to create your UiState
}.stateIn(
  viewModelScope,
  WhileSubscribed(5.seconds),
  initialUiState,
)
g
ah, so that means cancelling that flow will cancel any coroutineScope created inside that
flow {}
, giving the cancellation mechanism directly to the flows coming from that client. Nice, trying that instead 👍
👍 1
c
tangent... why doesn't this
Copy code
val viewModel: SomeViewModel = hiltViewModel()
need to be wrapped in a remember?
s
The samples don’t remember it https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:hilt/hil[…]edNavComposable&amp;ss=androidx%2Fplatform%2Fframeworks%2Fsupport The function itself is composable, so will skip if the inputs don’t change https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:hilt/hil[…]q=hiltViewModel&amp;ss=androidx%2Fplatform%2Fframeworks%2Fsupport And if you look inside, it seems like it gets the viewModel from inside the viewModelStoreOwner which should return he same VM on each invocation https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycl[…]mpose.viewModel&amp;ss=androidx%2Fplatform%2Fframeworks%2Fsupport Also, if you just
remember
it without the right keys, in case
LocalViewModelStoreOwner
changes you will still be getting the old reference right?
👀 1
Actually, composable functions that return a value aren’t skippable it seems https://issuetracker.google.com/issues/206021557 so I assume this is okay since the function always gets the exact same reference to the ViewModel, therefore it’s no issue to call it multiple times. In any case, if you do remember it, be careful not to keep a stale reference to it by accident.
g
@Stylianos Gakis The issue I see using
stateIn
instead of a backed
MutableStateFlow
is that I can't update as before from multiple places. Does that mean I should indeed have different flows in my ViewModel maybe? 🤔
s
What is “multiple places” here which can’t all be done inside where you construct your flow by using combine etc? Is this example something that would qualify as “changing the UiState from multiple places”?
g
Ah I see, you're using multiple MutableStateFlows combined there
s
In this one in particular, if you notice, the first
flow {}
in the
merge
there is only there so that it will re-start when the entire StateFlow goes from cold to hot again. Kinda what you’re asking for, scoping some work to happen when there’s a collector.
g
We have many actions the user can perform on the screen that update the same MutableStateFlow
s
Yes, since those are not doing any work which is always hot, they just store a hot value, but that’s okay
Is it perhaps that those actions can update the value stored inside that MutableStateFlow as you say, but then you use that value to kick off some expensive work as part of your combine() chain which will eventually be turned hot through
stateIn()
which means that if no collectors exist that chain will simply not be running?
g
Sounds promising!