eimmer
eimmer

Reputation: 1709

ViewModel triggered navigation with JetpackCompose

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

Answers (2)

bh_earth0
bh_earth0

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

z.g.y
z.g.y

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

Related Questions