ViewModels should not expose suspending functions
Use best practices to avoid exposing suspend functions from ViewModels
Why
Sometimes the developer may add a suspend
keyword to a function that calls another suspending function as that's the first auto-suggestion/warning provided by the IDE.
IDE makes it easy for the developer to auto-fix the warning by adding the suspend
keyword to the function.
But, this breaks the rule Classes extending "ViewModel" should not expose suspending functions.
Views should not be responsible for directly triggering coroutines. Hence, ViewModel classes should prefer creating coroutines instead of exposing suspending functions to perform some piece of business logic. This approach allows for easier testing of your application, as ViewModel classes can be unit tested, whereas views require instrumentation tests.
What to do
When you find a function in a ViewModel which breaks this rule, follow these steps to refactor it:
- Find the need for the
suspend
keyword in the function, i.e., check if the function is calling any other suspending function- e.g.,
listOfPlants.collectLatest
is a suspending function
- e.g.,
- See if you can wrap the suspending function call in a coroutine & use the appropriate scope & dispatcher
- e.g.,
viewModelScope.launch
can be used to wrap the suspending function call
- e.g.,
- Remove the
suspend
keyword from the function - Remove creating coroutines from the caller of the function if it is not needed anymore
How to do
The example,
// In ViewModel
suspend fun listenForUpdates() {
listOfPlants.collectLatest { plants ->
plants.filter { it.isFavorite }.let { favorites ->
// do something with favorites
}
}
}
// In Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launchWhenStarted {
viewModel.listenForUpdates()
}
}
could be refactored to,
// In ViewModel
fun listenForUpdates() {
viewModelScope.launch {
listOfPlants.collectLatest { plants ->
plants.filter { it.isFavorite }.let { favorites ->
// do something with favorites
}
}
}
}
// In Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launchWhenStarted {
viewModel.listenForUpdates()
}
}