The execute
method from RainbowCakeViewModel
is used to launch a coroutine on the UI thread with the appropriate CoroutineScope
in ViewModels.
A basic example of this:
class UserViewModel @Inject constructor(
private val userPresenter: UserPresenter
) : RainbowCakeViewModel<UserViewState>(Loading) {
fun loadUser(id: String) = execute {
// This code passed to `execute` is in a coroutine, and can call
// suspending methods of the Presenter. It can also update the viewState,
// as it's on the UI thread at this level of the call stack.
val user = userPresenter.getUser(id) // suspending call
viewState = UserLoaded(user)
}
}
By default, only one execute
call can be in progress for a given ViewModel at a time. This behaviour is present for situations where asynchronous work is to be performed based on user input, and the UI is supposed to wait for the result, in the sense that it shouldn’t launch additional jobs while the async work is ongoing.
To put it simply, if you have a button that launches a 1 second disk write in the background and then updates the view, this prevents the user from pressing the button 5 times in a second, resulting in 5 concurrent disk writes and 5 view updates.
The implementation does not make execute
calls sequential - it just terminates additional execute
calls if there’s already one ongoing.
Again, to put it simply, it just throws away the additional 4 button presses by the user while the first disk write is still ongoing.
A rather simplified version of the implementation of this blocking mechanism:
launch {
if (busy) {
Timber.d("Denying job launch, busy")
return@launch
}
busy = true
try {
task() // your coroutine block passed to execute
} finally {
busy = false
}
}
In the real implementation, you also get the option to disable this behaviour, and run multiple execute
calls freely in parallel. You just have to pass false
for the blocking
argument of the execute
method:
execute(blocking = false) {
somePresenter.performWork()
}
There are some variants of the regular execute
method for special use cases.
executeNonBlocking
This is a convenience method that’s exactly equivalent to calling execute
with the blocking = false
parameter.
executeCancellable
This method is the exact same as the execute
method, except it returns the Job
that it has started, which enables ViewModel code to manually cancel it, if necessary. The regular execute
method has a Unit
return value, since most of the time it’s supposed to be used with an expression body ViewModel function, which would leak the Job
to the Fragment by default:
fun load() = execute { // This method returns the result of `execute` to the Fragment!
// ...
}
Since executeCancellable
returns the Job
, it should never be used in an expression body. Instead, write a regular function in these cases:
fun load() {
// Set a property of the ViewModel, or do whatever you need the Job for
job = executeCancellable {
// ...
}
}
Any exceptions that happen inside execute
calls that aren’t caught will be, by default, caught by the execute
function. These are logged by default. Job cancellation exceptions (these happen when the ViewModel
is cleared and the job is cancelled as a consequence) are logged separately.
try {
task()
} catch (e: CancellationException) {
log("Job cancelled exception:")
log(e)
} catch (e: Exception) {
log("Unhandled exception in ViewModel:")
log(e)
}
This behaviour can be modified by changing the framework’s configuration, as described here. It’s recommended that you disable this catch-all clause at least in debug builds.