This page looks best with JavaScript enabled

Play with Coroutine in Kotlin & Android

 ·  ☕ 3 min read · 👀... views

Imagine a Rock - Paper - Scissor game, it has a player versue a Host. When the game starts, the Host will wait for the player forming his/her hand shape. And while waiting, let the Host continuously forms his hand (to make it not boring).

Make an infinite loop

The pseudocode might look like this:

while (playerThinking)
    randomly form a hand shape
    show it to player

In Android, we have a main looper that also handles UI. If we want another infinite loop, we must not create the loop in main thread, as it’ll block the UI, which then leads to ANR (Application Not Responding).

Instead, we should create the infinite loop in another thread by using AsynTask, Handler#post() or Kotlin’s coroutine…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private fun gameLoop() {
    viewModelScope.launch {
        while (true) {
            delay(DELAYED_PLAY)                     // #1
            when (gameState) {
                GameState.PLAYER_THINKING -> {
                    randomHostPlay()                // #2
                }
                ...
            }
        }

    }
}
  • #1: Add some millisecond delay to let CPU relaxed.
  • #2: The function contains logic for forming the Host’s hand.

Make a blocked invocation

When the player and the Host have formed their hands, it’s time to decide who wins the round.
Assume the computation for that decision is very long, this computation should provide two things:

  • It should not block the UI thread.
  • It should notify the result to the Host.

One approach for solving this problem is to use a callback: The computation is brought out of main thread. And after its computation, it invokes a callback, fall back to the Host.

fun decideWinOrLose(callback)
    do a long heavy computation
    get result, invoke callback.onResult(result)
...
//usage
callback = {
    onResult(result) = {
        announce this result
    }
}
decideWinOrLose(callback)

This is fine when you only have a computation. But what if you have a chains of computation, each depends on the previous one? The callback creation might become messy.

In that case, let’s convert callback to a blocked invocation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
suspend fun waitGameResult(): GameScore = suspendCancellableCoroutine { cont ->
    callback = object : IOnGameResult {                     // #1
        override fun onGameResult(result: GameScore) {
            callback = null
            cont.resume(result)                             // #2
            gameState = GameState.OBSERVE_RESULT
        }
    }
    cont.invokeOnCancellation { callback = null }
    gameState = GameState.DECIDE_WIN_LOSE
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//producer
private fun decideWinOrLose() {
    // long computation
    callback.onGameResult(result)                           // #3
}

//consumer
viewModelScope.launch {
    val result = waitGameResult()                           // #4
    sendToHost(result)
}
  • At #1: A callback object is created and managed by a producer.
  • At #4, when the function waitGameResult exits, the calling thread is blocked/suspended. When the long computation finishes at #3, the callback is invoked. Then at #2, the cont object invokes its resume() function, which will unblock the calling thread at #4, the result is returned from resume() function, and is sent to the host.
Share on