Android Developers
Published in

Android Developers

The suspend modifier — under the hood

Kotlin Vocabulary: Coroutines

Coroutines 101

// Simplified code that only considers the happy path
fun loginUser(userId: String, password: String, userResult: Callback<User>) {
// Async callbacks
userRemoteDataSource.logUserIn { user ->
// Successful network request
userLocalDataSource.logUserIn(user) { userDb ->
// Result saved in DB
userResult.success(userDb)
}
}
}
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}

Suspend under the hood

suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}
// UserRemoteDataSource.kt
suspend fun logUserIn(userId: String, password: String): User
// UserLocalDataSource.kt
suspend fun logUserIn(userId: String): UserDb

Continuation interface

interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(value: Result<T>)
}
  • context will be the CoroutineContext to be used in that continuation.
  • resumeWith resumes execution of the coroutine with a Result, that can contain either a value which is the result of the computation that caused the suspension or an exception.
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
completion.resume(userDb)
}
  • When converting callback-based APIs to coroutines using suspendCoroutine or suspendCancellableCoroutine (which you should always prefer), you directly interact with a Continuation object to resume the coroutine that got suspended after running the block of code passed as a parameter.
  • You can start a coroutine with the startCoroutine extension function on a suspend function. It takes a Continuation object as a parameter that will get called when the new coroutine finishes with either a result or an exception.

Using different Dispatchers

The generated State machine

fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
// Label 0 -> first execution
val user = userRemoteDataSource.logUserIn(userId, password)
// Label 1 -> resumes from userRemoteDataSource
val userDb = userLocalDataSource.logUserIn(user)
// Label 2 -> resumes from userLocalDataSource
completion.resume(userDb)
}
fun loginUser(userId: String, password: String, completion: Continuation<Any?>) {
when(label) {
0 -> { // Label 0 -> first execution
userRemoteDataSource.logUserIn(userId, password)
}
1 -> { // Label 1 -> resumes from userRemoteDataSource
userLocalDataSource.logUserIn(user)
}
2 -> { // Label 2 -> resumes from userLocalDataSource
completion.resume(userDb)
}
else -> throw IllegalStateException(...)
}
}
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {  class LoginUserStateMachine(
// completion parameter is the callback to the function
// that called loginUser
completion: Continuation<Any?>
): CoroutineImpl(completion) {
// Local variables of the suspend function
var user: User? = null
var userDb: UserDb? = null
// Common objects for all CoroutineImpls
var result: Any? = null
var label: Int = 0
// this function calls the loginUser again to trigger the
// state machine (label will be already in the next state) and
// result will be the result of the previous state's computation
override fun invokeSuspend(result: Any?) {
this.result = result
loginUser(null, null, this)
}
}
...
}
fun loginUser(userId: String?, password: String?, completion: Continuation<Any?>) {
...
val continuation = completion as? LoginUserStateMachine ?: LoginUserStateMachine(completion) ...
}
  • The when statement’s argument is the label from inside the LoginUserStateMachine instance.
  • Every time a new state is processed, there’s a check in case a failure happened when this function was suspended.
  • Before calling the next suspend function (i.e. logUserIn), the label of the LoginUserStateMachine instance is updated to the next state.
  • When inside this state machine there’s a call to another suspend function, the instance of the continuation (of type LoginUserStateMachine) is passed as a parameter. The suspend function to be called has also been transformed by the compiler and it’s another state machine like this one that takes a continuation object as a parameter! When the state machine of that suspend function finishes, it will resume the execution of this state machine.
suspend fun loginUser(userId: String, password: String): User {
val user = userRemoteDataSource.logUserIn(userId, password)
val userDb = userLocalDataSource.logUserIn(user)
return userDb
}

--

--

Articles on modern tools and resources to help you build experiences that people love, faster and easier, across every Android device.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store