Search code examples
androidandroid-jetpack-composekotlin-coroutinesandroid-viewmodel

What is the proper way to navigate between screens in jetpack compose while there's coroutines jobs to be executed in the viewmodel?


Problem:-

I have a composable WelcomeScreen that shows a finish button at the end and when the user clicks the finish button it calls a function from the WelcomeScreenViewModel that launches a viewModelScope coroutine to save a boolean value using android DataStore.

The problem I'm facing is that when the button is clicked the navigation works but the WelcomeScreenViewModel doesn't launch, after some digging I understood the problem is caused by NavController.popBackStack() which removes the WelcomeScreen from the stack which in turn kills the WelcomeScreenViewModel which in turn due to viewModelScope being a lifecycle aware component it cancels all the coroutines jobs related to the viewModel when onClear() is called which doesn't give the chance for the coroutine to launch before onClear() is called.

Solutions I tried:-

  • I have tired to call NavController.popBackStack() from a rememberCoroutineScope().launch block which allowed for the ViewModel coroutine to be launched but transition from WelcomeScreen to the next screen didn't work.

  • I tried calling NavController.popBackStack() after the call to WelcomeScreenViewModel coroutine but that didn't make any difference.

I'm stuck, I've been trying to find a workaround and fix this issue for hours and banging my head against the wall.

Here's some code:-

WelcomeScreen

@Composable
fun WelcomeScreen(
    navHostController: NavHostController,
    welcomeViewModel: WelcomeScreenViewModel = hiltViewModel()
) {

    val pages = listOf(
        WelcomeScreenPages.PageContent(
            imageResource = R.drawable.greetings,
            title = stringResource(id = R.string.greeting_page_title),
            description = stringResource(R.string.greetings_page_description)
        ),
        WelcomeScreenPages.PageContent(
            imageResource = R.drawable.explore,
            title = stringResource(R.string.explore_page_title),
            description = stringResource(R.string.explore_page_description)
        ),
        WelcomeScreenPages.PageContent(
            imageResource = R.drawable.power,
            title = stringResource(R.string.power_page_title),
            description = stringResource(R.string.power_page_description)
        )
    )

    val pagerState = rememberPagerState()

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(color = welcomeScreenBackgroundColor)
    ) {
        HorizontalPager(
            modifier = Modifier.weight(10f),
            count = pages.size,
            state = pagerState
        ) { page ->
            Page(welcomeScreenPage = pages[page])
        }

        HorizontalPagerIndicator(
            modifier = Modifier
                .weight(1f)
                .align(Alignment.CenterHorizontally),
            pagerState = pagerState,
            activeColor = activeIndicatorColor,
            inactiveColor = inactiveIndicatorColor,
            indicatorWidth = PAGING_INDICATOR_WIDTH,
            spacing = PAGING_INDICATOR_SPACING
        )

        FinishButton(
            modifier = Modifier
                .weight(1f),
            pagerState = pagerState,
            lastPage = pages.size - 1
        ) {
            navHostController.popBackStack()
            navHostController.navigate(Screen.Home.route)
            welcomeViewModel.welcomePageCompleted(isCompleted = true)
        }
    }
}

WelcomeScreenViewModel

@HiltViewModel
class WelcomeScreenViewModel @Inject constructor(private val useCase: UseCase): ViewModel(){

    private val TAG = "WelcomeScreenViewModel"

    fun welcomePageCompleted(isCompleted: Boolean){
        viewModelScope.launch(Dispatchers.IO) {
            useCase.welcomePageCompletedUseCase(isCompleted = isCompleted)
            Log.i(TAG, "Coroutine triggered! ")
            Log.i(TAG,"welcome page state saved! $isCompleted")
        }
    }
}

Solution

  • You need to call popBackStack after the coroutine in ViewModel finishes. When you launch a coroutine, you have to keep in mind it is non-blocking.

    in your viewmodel

    
    //signal your view when the coroutine finishes through state
    val finished by mutableStateOf(false)
    
    //in your coroutine
    viewModelScope.launch(Dispatchers.IO) {
        useCase.welcomePageCompletedUseCase(isCompleted = isCompleted)
    
        //change state to indicate coroutine has finished
        finished = true
    
        Log.i(TAG, "Coroutine triggered! ")
        Log.i(TAG,"welcome page state saved! $isCompleted")
    }
    

    In your view you need to react if the state changes, do not call popBackStack immediately on button click, just call your coroutine.

    
    LaunchedEffect(viewModel.finished) {
        if(viewModel.finished) {
            navHostController.popBackStack()
        }
    }
    
    

    Read more: Here is an excellent article on handling events from composables.