Search code examples
androidandroid-jetpack-composeside-effectsjetpack-compose-navigation

Compose side effects + Jetpack navigation + onBackPressed = Stuck navigation


I am having this issue where I have to navigate when given state gets updated after an asynchronous task gets executed. I am doing it like this:

At ViewModel.kt
fun executeRandomTask() {
    viewModelScope.launch {
        runAsyncTask()
        state = Success
    }
}

At Composable.kt
LaunchedEffect(viewModel.state) {
    if(viewModel.state is Success) {
        navController.navigate("nextScreen")
    }
}

Then in the next screen, I click the back navigation button (onBackPressed) and what happens, is that the effect gets launched again. So I end up again in "nextScreen".

When I do this next workaround:

DisposableEffect(viewModel.state) {
    if(viewModel.state is Success) {
        navController.navigate("nextScreen")
    }
    onDispose {
        viewModel.state = null 
    }
}

Like this, the viewmodel state gets cleared and it also proves that what is happening is that the navigation controller destroys the previous screen (not sure if it is the intended behavior).

I am not sure about what I should be doing to avoid this, since this is a pretty common scenario and having to clear the state after a certain state is reached looks dirty.


Solution

  • I use SharedFlow for emitting one-time events like this

    class MyViewModel : ViewModel() {
    
        private val _eventFlow = MutableSharedFlow<OneTimeEvent>()
        val eventFlow = _eventFlow.asSharedFlow()
    
        private fun emitEvent(event: OneTimeEvent) {
            viewModelScope.launch { _eventFlow.emit(event) }
        }
    
    }
    

    The sealed class for defining events

    sealed class OneTimeEvent {
        object SomeEvent: OneTimeEvent()
    }
    

    And finally in the Screen collect the flow like this

    fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
        LaunchedEffect(Unit) {
            viewModel.eventFlow.collect { event ->
                when(event){
                    SomeEvent -> { 
                        //Do Something 
                    }
                }
            }
        }
    }
    

    So whenever your state changes you can emit some event and take action against it in your Screen.