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")
}
}
}
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.