I have a view model injected with hilt that controls the state of a loading spinner, which is a fullscreen dialog.
@HiltViewModel
class MyViewModel @Inject constructor(
val coordinator: MyCoordinator
) : ViewModel() {
private val _isChanging = MutableStateFlow(false)
val isChanging = _isChanging.asStateFlow()
fun change() {
_isChanging.value = true
// suspend function which takes about 5 seconds to resolve
coordinator.change()
_isChanging.value = false
}
}
MyCoordinator
val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun change() {
applicationScope.launch {
// do something for 5 seconds
delay(5000)
}
}
And finally the ui
@Composable
fun MyScreen(
viewModel: MyViewModel = hiltViewModel()
) {
val changing by viewModel.isChanging.collectAsStateWithLifecycle()
if (changing) {
AnimatedLoadingSpinner()
}
// other ui, including a button that triggers viewModel::change in the onClick lambda
}
When I press the button, the change function does indeed run, But the loading spinner never shows. How can I get the loading spinner to display?
I have a feeling it has something to do with the coroutine in the coordinator running. I think when change gets called, it sets isChanging to true, launches the coroutine, and then immediately sets isChanging back to false. But I don't know how to fix this, other than pushing the applicationScope into the viewmodel, or passing the isChanging.value = x into the change function as lambdas so it can be called in the applicationScope.
launch
launches a new coroutine and immediately returns. The new coroutine runs asynchronously to the current code, that's what it's for. But that also means that the remaining code doesn't wait for that coroutine to finish. So _isChanging.value = false
is called directly after the coroutine was launched, not after it finished.
There are multiple ways to restructure your code to achieve what you want. From the top of my head, but there are probably much more:
coordinator.change
a suspend function and launch the coroutine from the view model (with the viewModelScope
). Then you can put the updates to _isChanging
inside the launch
block.coordinator.change
that is executed after the delay
. I would not recommend this, though, it doesn't conform to clean architecture principles._isChanging
flow anymore.