101010

プログラミング備忘録とともに、ポエムってます。

非同期処理でCoroutine(コルーチン)を使ってみる | Android Kotlin

いよいよ非同期処理もコルーチンへ移行しようと思っているので理解できている範囲で使い方をまとめておく。

ライブラリのインストール

Androidアプリでコルーチンを使えるようにするためにGradleでインストールする。

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

ScopedAppActivityクラスの作成

公式ドキュメントにあるようにAppCompatActivityを継承したScopedAppActivityクラスを作る。 kotlinx.coroutines/coroutines-guide-ui.md at master · Kotlin/kotlinx.coroutines · GitHub

abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope by MainScope() {

    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

次のようにしてコルーチンを使うアクティビティでScopedAppActivityを継承するようにする。

class MainActivity : ScopedAppActivity() {
   ...
}

これにより、onDestroyでコルーチンのJobキャンセルし忘れがなくなり、記述も簡潔に書けるようになる。

launchを使う

launch {
    // メインスレッドで実行される
    var counter = 0
    while (true) {
        textView.text = "${++counter} $message"
        delay(1000) // ブロッキングされない
    }
}

新しいコルーチンを起動するためにはこのようにlaunchを使う。引数を指定していないのでブロック内はメインスレッドとなる。 またdelayはコルーチンを中断する関数で次のようにsuspendで定義されている。

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
    }
}

コルーチンで非同期処理を書く

button.setOnClickListener {
    launch {
        val deferred = async(Dispatchers.IO) {
            Thread.sleep(3000)
            "リクエスト No.${++requestCount}"
        }
        withContext(Dispatchers.Main) {
            message = deferred.await()
        }
    }
}

asyncを使って非同期処理を書く。awaitを使うことで実行結果を待つことが出来る。UIにアクセスする場合はwithContext(Dispatchers.Main) ブロック内に書くこと。ただし上記の場合はwithContext(Dispatchers.Main)の記述が無くても問題なく動く。asyncブロック内の最後の行が戻り値となる。returnをつけてはいけないことになっている。

suspend関数

コルーチンのasyncを使った非同期処理の部分を関数に移したい場合はsuspendを使う。

var requestCount = 0

suspend fun showIOData() {
    val deferred = async(Dispatchers.IO) {
        Thread.sleep(3000)
        "リクエスト No.${++requestCount}"
    }
    withContext(Dispatchers.Main) {
        message = deferred.await()
    }
}

button.setOnClickListener {
    launch {
        showIOData()
    }
}

asyncを使わずwithContextを使って書く。

先程のプログラムは次のようにも書ける。asyncを使った場合との厳密的な違いはわからない。suspendとwithContextを使ったこちらのほうが個人的には分かりやすいと思っている。

var requestCount = 0

suspend fun fetchIOData():String = withContext(Dispatchers.IO) {
    Thread.sleep(3000)
    "リクエスト No.${++requestCount}"
}

button.setOnClickListener {
    launch {
        message = fetchIOData()
    }
}

View.onClickを拡張する

button.setOnClickListener {
    launch {
        message = fetchIOData()
    }
}

のようにlaunchの記述がいちいち面倒な場合はサンプルドキュメントのようにView.onClickを拡張してしまう。

fun View.onClick(action: suspend (View) -> Unit) {
    val eventActor = GlobalScope.actor<View>(Dispatchers.Main, capacity = Channel.CONFLATED) {
        for (event in channel) action(event)
    }
    setOnClickListener {
        eventActor.offer(it)
    }
}

するとこのようにシンプルに書けるようになる。

button.onClick {
    message = fetchIOData()
}

ここまでのMainActivityのまとめ

ここまでのサンプルソースを載せておこう。

import android.os.Bundle
import android.util.Log
import android.view.View
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor

class MainActivity : ScopedAppActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setup()
    }



    fun setup() {

        var message = ""
        launch {
            // メインスレッドで実行される
            var counter = 0
            while (true) {
                textView.text = "${++counter} $message"
                delay(1000) // ブロッキングされない
            }
        }

        var requestCount = 0
        
        suspend fun fetchIOData():String = withContext(Dispatchers.IO) {
            Thread.sleep(3000)
            "リクエスト No.${++requestCount}"
        }

        button.onClick {
            message = fetchIOData()
        }


    }


    fun View.onClick(action: suspend (View) -> Unit) {
        val eventActor = GlobalScope.actor<View>(Dispatchers.Main, capacity = Channel.CONFLATED) {
            for (event in channel) action(event)
        }
        setOnClickListener {
            eventActor.offer(it)
        }
    }
}

suspend内のコルーチンのキャンセル

ベストプラクティスかどうかはわからないが、CoroutineScopeをメンバ変数に保持してcancelTaskを呼び出することでキャンセルさせることが出来る。

object EchoApiGenerator {
    var mTask:CoroutineScope? = null

    @Throws(IOException::class)
    suspend fun download(): String = withContext(Dispatchers.IO) {
        mTask = this

        ...非同期処理
    }

    fun cancelTask() {
        mTask?.cancel()
    }
}

キャンセルされたら例外が投げられるのでそれをフックする。ただし非同期処理でOkHttpの処理を書いてみたがOkHttpはキャンセルされなかった。その場合はOkHttpのCallをキャンセルさせたほうが良さそうだ。

try {
    message = CoroutineScope.download()
} catch (e:CancellationException) {
    println("コルーチンキャンセル! $e")
}

参考