rocketraman
04/17/2021, 12:17 PMLaunchedEffect
(that creates a toast), keying on a state variable (via by x.collectAsState()
). However, when the state is transient i.e. it changes relatively quickly from state A to state B, the LaunchedEffect
is not triggered at all for either state A or B.
Its a login screen and the user state transitions from a user login error to an anonymous user pretty quickly. I can make some changes to how the state is changed to avoid thus, but I'd like to understand what is going on here.Dominaezzz
04/17/2021, 12:45 PMLaunchedEffect
say not to use it like this.Dominaezzz
04/17/2021, 12:47 PMrememberCoroutineScope()
and launch
.Lilly
04/17/2021, 1:01 PMDominaezzz
04/17/2021, 1:11 PMonClick = { scope.launch { ... } }
.Adam Powell
04/17/2021, 1:45 PMrocketraman
04/17/2021, 4:52 PMFairly certain the docs of LaunchedEffect say not to use it like this.Ok, I'm new to compose so please be more specific. As I understood, a snackbar should be triggered in a
LaunchedEffect
-- in fact, I'm trying to use it pretty much exactly like this example: https://developer.android.com/jetpack/compose/lifecycle#launchedeffect.
By the sound of it, this code is confusing observable state vs observable events.That's again possible, but again I'm using it pretty much like the example above. My state has an error when the login fails, and then soon after the error is cleared from the state. Here is the code -- I've changed it to use
Snackbar
instead of toast, but it is not working:
@Composable
fun LoginSplash(userViewModel: UserViewModel) {
// viewState is a StateFlow
val userState by userViewModel.viewState.collectAsState()
val scaffoldState = rememberScaffoldState()
val errorReason = userState.errorReason
if (errorReason != null) {
println("--0-0-000-0- doing snackbar with reason=$errorReason")
LaunchedEffect(scaffoldState.snackbarHostState) {
scaffoldState.snackbarHostState.showSnackbar(messageFrom(errorReason), duration = SnackbarDuration.Long)
}
}
... other view stuff down here ...
}
The logs show:
D/UserStateTransformer: UserStateTransformer: BackendStateModel userState changed: state=UndefinedUser(user=dev.gitlive.firebase.auth.FirebaseUser@cef8d7, reason=ERROR)
I/System.out: --0-0-000-0- doing snackbar with reason=ERROR
... a few tens of milliseconds later...
D/UserStateTransformer: UserStateTransformer: BackendStateModel userState changed: state=com.xacoach.xascore.shared.api.AnonymousUser@27940e4
If I delay the state change to AnonymousUser
then things worked fine when I was using toast, but now that I've switched to Snackbar even that doesn't work any more -- nothing shows at all either way.
I'm entirely sure I'm missing something quite basic, but I don't know what it is.rocketraman
04/17/2021, 5:05 PMrocketraman
04/17/2021, 5:10 PMAdam Powell
04/17/2021, 5:25 PMerrorReason
unless you recompose with a null
in between to remove the LaunchedEffect
rocketraman
04/17/2021, 5:26 PMAdam Powell
04/17/2021, 5:26 PMerrorReason
as an additional key parameter to LaunchedEffect
, so that if errorReason
changes the old show is cancelled and it will show the new reasonrocketraman
04/17/2021, 5:27 PMif
check surrounding it. I'm actually not sure why the if
is required.Adam Powell
04/17/2021, 5:29 PMif
there so that there's no LaunchedEffect
if there's no error to showrocketraman
04/17/2021, 5:30 PMrocketraman
04/17/2021, 5:36 PMAdam Powell
04/17/2021, 5:36 PMrocketraman
04/17/2021, 5:39 PMrocketraman
04/17/2021, 5:40 PMrocketraman
04/17/2021, 5:42 PMrocketraman
04/17/2021, 5:46 PMNonCancellable
, but that doesn't solve the problem. As I said in my original post, the LaunchedEffect seems to not be executed at all, or it is but it is cancelled immediately.Adam Powell
04/17/2021, 5:50 PMcollectAsState
conflates incoming values, so new values that are emitted before recomposition will replace old as if they never happened. This is as intended, as state is always meant to be declarative and idempotentAdam Powell
04/17/2021, 5:50 PMAdam Powell
04/17/2021, 5:51 PMLaunchedEffect(errorFlow, snackbarHostState) {
errorFlow.collect {
snackbarHostState.showSnackbar(messageFrom(errorReason), duration = SnackbarDuration.Long)
}
}
Adam Powell
04/17/2021, 5:52 PMerrorFlow
to provide queuing, presuming your flow supports that kind of backpressureAdam Powell
04/17/2021, 5:53 PMStateFlow
under the hood that you're breaking an error state off from, then the StateFlow is also going to conflate values in a similar way for the same reasonsrocketraman
04/17/2021, 6:07 PMSharedFlow
, which doesn't conflate as I understand it, for just the error event, along with a collect for the snackbar.rocketraman
04/17/2021, 6:08 PMrocketraman
04/18/2021, 2:03 AMSharedFlow
(vs a StateFlow
to avoid conflation), along with a manual collect
inside LaunchedEffect
. Note that using collectAsState
on the new SharedFlow
also did not work, as was expected due to the conflation by collectAsState
, nor did using a manual collect on the existing StateFlow
.
I wonder if it might be valuable to abstract this complexity with a Compose function like fun <T> SharedFlow<T>.collectAsUnconflatedState()
. I would imagine acting on events like this is not an unusual requirement.