Lokik Soni
07/26/2022, 6:00 PMval isAlarmOn by viewModel.alarmBtnState.collectAsState()
In ViewModel
val alarmBtnState = isAlarmActivated(Unit).map {
it.successOr(DEF_VAL_ALARM_ACTIVATED)
}.stateIn(viewModelScope, WhileViewSubscribed, DEF_VAL_ALARM_ACTIVATED)
Alex Vanyo
07/26/2022, 6:18 PMcollectAsState
by itself will continue to collect even if the application is in the background.
https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda#27a9 describes how to make a flow that is lifecycle aware, and androidx.lifecycle
2.6.0-alpha01
added a collectAsStateWithLifecycle
utility to accomplish this as well!Oleksandr Balan
07/27/2022, 3:45 PMState
inside VM? How could we then make it lifecycle-aware?Alex Vanyo
07/27/2022, 5:22 PMState
inside a ViewModel
, then I’m assuming you have some other suspending method that is updating the State
that you want to cancel when the lifecycle changes.
The most straightforward way to do that would be to expose the suspend fun
from the ViewModel
directly, and use repeatOnLifecycle
inside the LaunchedEffect
. Something like:
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(viewModel, lifecycleOwner) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.update()
}
}
This fundamentally does the same thing as the StateFlow
approach. The ViewModel
is exposing 2 things: a state, and a way to initiate and cancel updating that state, based on the Lifecycle
.
The difference, is that for the StateFlow
, the state and the method to initiate and cancel updating are tied together in the StateFlow
object. For the Compose State
, the State
object exists separately, and there is a different suspend fun
that will update the State
.Oleksandr Balan
07/27/2022, 5:26 PMState
. So I guess this part should be changed if we want to make State
aware of different lifecycle 🤔Alex Vanyo
07/27/2022, 6:02 PMState
itself is completely unaware of lifecycle. It just exists. You can always read the current value, transform it, or pass it around elsewhere in your state production pipeline.
What is modifying the State
can be lifecycle aware, and when you want it active depends on your use case. You might want to have that be a suspend fun
launched in the ViewModel
scope, if you have a request that you want to continue executing while an Activity recreation is happening. Or you might want to cancel long running database connections if the application goes to the background.Lokik Soni
07/27/2022, 8:20 PMLokik Soni
07/27/2022, 8:23 PMThe MutableStateFlow and MutableSharedFlow APIs expose a subscriptionCount field that you can use to stop the underlying producer when subscriptionCount is zero. By default, they will keep the producer active as long as the object that holds the flow instance is in memory. There are some valid use cases for this though, for example, a UiState exposed from the ViewModel to the UI using StateFlow. That's ok! This use case demands the ViewModel to always provide the latest UI state to the View.
Similarly, the Flow.stateIn and Flow.shareIn operators can be configured with the sharing started policy for this. WhileSubscribed() will stop the underlying producer when there are no active observers! On the contrary, Eagerly or Lazily will keep the underlying producer active as long as the CoroutineScope they use is active.
Alex Vanyo
07/27/2022, 11:24 PMWhileViewSubscribed
is custom, is it probably something like WhileSubscribed(5000)
? That will keep collecting alive for an additional 5 seconds after being cancelled. So the upstream flow won’t be cancelled if the UI goes away and comes back quickly (say, during activity recreation), but it will eventually be canceled if the application goes into the background for a while.
https://medium.com/androiddevelopers/things-to-know-about-flows-sharein-and-statein-operators-20e6ccb2bc74 to go in more detail about those APIs and explains the WhileSubscribed(5000)
option.Lokik Soni
07/28/2022, 12:47 PMAlex Vanyo
08/10/2022, 5:23 PMcollectAsStateWithLifecycle
, and why you’d want to use it over `collectAsState`:
https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3
Hopefully that helps give some more context!Michal
08/19/2022, 8:05 AM...
val state by viewModel.state.collectAsStateWithLifecycle()
LaunchedEffect(key1 = state.isAuthenticated) {
if (!state.isAuthenticated) {
showLogin()
}
}
...
With collectAsState the viewmodel gets updated in the background and when the UI is shown, it will have the correct state. However, when using collectAsStateWithLifecycle, it won't update and if UI is shown, it will still forward with the LaunchedEffect as state.isAuthenticated is false (even though in next ms it would be true, but at that time it is already paused again and we are moved to login screen).
How would you prevent this from happening? I suspect this will be pretty common, because this kind of "login" is suggested by google and very common.
Thank you.Alex Vanyo
08/19/2022, 4:36 PMcollectAsState
or collectAsStateWithLifecycle
I’d take the approach of having some sort of intermediate loading state, which you’d probably want anyway if knowing whether you are authenticated or not is asynchronous due to disk I/O (if for example it’s coming from datastore
)
Right now, I’m assuming that the default is isAuthenticated
is false
, but you could have something like loginState
that is Unknown
, Authenticated
, Unauthenticated
, and you’d only showLogin()
if Unauthenticated
.Michal
08/19/2022, 6:21 PMAlex Vanyo
08/19/2022, 6:35 PMUnknown
as a no-op. So you’d only redirect to the login screen if Unauthenticated
. If Unknown
, you’d keep waiting (showing a loading state of some sort) and then redirecting if it becomes Unauthenticated
and showing the screen as normal if Authenticated
So we have that loading state not on every screen, but only on the login screen.What happens if you’re further in the app, and then your app is backgrounded and killed due to process death? When the user returns to the app, they’ll still be further in the app, but the current log in state won’t be available synchronously there. At any destination, the app could be recreated with a fresh process, so you can’t assume you’ve been to any other destination to already have handled some form of asynchronous loading.
Michal
08/19/2022, 6:47 PMAlex Vanyo
08/19/2022, 9:56 PMA is shown, but gets state Unauthenticated (it couldn’t be updated because of lifecycle)Ah I think I understand the issue: the state flow for screen A is using a stale value for authentication, which will be reused (and trigger the effect again) before it has a chance to see the updated value. I think this is a use case for
replayExpirationMillis = 0
as part of SharingStrategy.WhileSubscribed
or onSubscription
.
Essentially, you want to avoid using that stale value in the case of authentication, so that it instead goes back to unknown when not subscribed, until it gets a fresh, updated value.Alex Vanyo
08/19/2022, 10:36 PMMichal
08/20/2022, 6:11 AM