Reputation: 1709
In Android I often want to navigate is response to state change from a ViewModel. (for example, successful authentication triggers navigation to the user's home screen.)
Is the best practice to trigger navigation from within the ViewModel? Is there an intentional mechanism to trigger navigation within a composable in response to a ViewModel state change?
With Jetpack Compose the process for handling this use case is not obvious. If I try something like the following example navigation will occur, but the destination I navigate to will not behave correctly. I believe this is because the original composable function was not allowed to finish before navigation was invoked.
// Does not behave correctly.
@Composable fun AuthScreen() {
val screenState = viewModel.screenState.observeAsState()
if(screenState.value is ScreenState.UserAuthenticated){
navController.navigate("/gameScreen")
} else {
LoginScreen()
}
}
I do observe the correct behavior if I use LauncedEffect as follows:
// Does behave correctly.
@Composable fun AuthScreen() {
val screenState = viewModel.screenState.observeAsState()
if(screenState.value is ScreenState.UserAuthenticated){
LaunchedEffect(key1 = "test") {
navController.navigate("$/gameScreen")
}
} else {
LoginScreen()
}
}
Is this correct? The documentation for LaunchedEffect states the following, but the meaning is not 100% clear to me:
When LaunchedEffect enters the composition it will launch block into the composition's CoroutineContext. The coroutine will be cancelled and re-launched when LaunchedEffect is recomposed with a different key1, key2 or key3. The coroutine will be cancelled when the LaunchedEffect leaves the composition.
Upvotes: 7
Views: 1992
Reputation: 2844
thanks z.g.y
but i couldnt use it like that.
i get Error: @Composable invocations can only happen from the context of a @Composable function
i used it like this;
//fix - endless loop
LaunchedEffect(
key1 = "FetchInfo_UpdateGui_Compose",
FetchInfo_UpdateGui_Compose(uiState, event)
)
Long Code:
i hate it when people dont put full code.. where am i gonna write the code. which file??
here:
package com.axiel7.moelist.ui.userlist
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun myListView(
uiState: UserMediaListUiState,
event: UserMediaListEvent?,
navActionManager: NavActionManager,
nestedScrollConnection: NestedScrollConnection? = null,
contentPadding: PaddingValues = PaddingValues(),
onShowEditSheet: (BaseUserMediaList<out BaseMediaNode>) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val haptic = LocalHapticFeedback.current
val pullRefreshState = rememberPullToRefreshState()
//fix - endless loop
LaunchedEffect(
key1 = "FetchInfo_UpdateGui_Compose",
FetchInfo_UpdateGui_Compose(uiState, event)
)
//below is your Text() , button()
//....
}
```kotlin
Upvotes: 0
Reputation: 6257
This code
// Does not behave correctly.
@Composable fun AuthScreen() {
val screenState = viewModel.screenState.observeAsState()
if(screenState.value is ScreenState.UserAuthenticated){
navController.navigate("/gameScreen")
} else {
LoginScreen()
}
}
which does not behave correctly is most likely causing an issue like this, and one of the ways to solve it is by this
// Does behave correctly.
@Composable fun AuthScreen() {
val screenState = viewModel.screenState.observeAsState()
if(screenState.value is ScreenState.UserAuthenticated){
LaunchedEffect(key1 = "test") {
navController.navigate("$/gameScreen")
}
} else {
LoginScreen()
}
}
which behaves correctly, because LaunchedEffect
is guaranteed to execute only once per composition
assuming its key
won't change on the next composition pass, otherwise it will keep executing on every update of its composable scope.
I would suggest considering the "correct" not only based on suggested components but thinking how to avoid navigation pitfalls like the link I provided.
It won't matter if it's coming from a ViewModel
or some flow emissions but the idea for a safe navigation in compose (so far as I understand it) is to make sure that the navigation call will only happen in a block that will never re-execute on succeeding re-compositions
, which this one also suffers from the first type of code above.
Upvotes: 2