Kotlin でコルーチンをはじめよう

 

Kotlinで非同期処理Coroutine(コルーチン)の使い方
Kotlinで非同期処理Coroutine(コルーチン)の使い方

コルーチン (Coroutine) とは、Kotlin で並列処理を非同期におこなえる軽量なスレッドです。Android アプリ開発でもコルーチンを使えるので、今までやっていた非同期処理を、Kotlin のコルーチンに変えてみようと思いました。本記事では、Android開発におけるコルーチンの使い方を理解できている範囲でまとめてみました。

▼ こちらの書籍でもCoroutineの使い方が解説されてますのでご参考になさってみてください。

Coroutine関連の本は少ないですが、Amazonにて 「Coroutine Kotlin 本」 で検索すると、英語の書籍がヒットします。

非同期処理のコルーチンを可能にするライブラリのインストール

Androidアプリでコルーチンを使えるようにするため、次のようにしてコルーチンのモジュールをGradleでインストールします。

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

ScopedAppActivityクラスの作成

こちらの公式ドキュメントを参考にプログラムを解説していきます。

公式ドキュメントにあるようにAppCompatActivityを継承したScopedAppActivityクラスを作ります。

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

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

コルーチンを使うアクティビティでScopedAppActivityを継承させます。ScopedAppActivityによって、onDestroyでコルーチンのJobキャンセルをし忘れることがなくなり、コルーチンの記述も簡潔に書けるようになります。

class MainActivity : ScopedAppActivity() {
   ...
}

launchを使う

新しいコルーチンを起動するにはlaunchを使います。引数を指定していないのでブロック内はメインスレッドなことに気をつけましょう。

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

また、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)
    }
}

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

次に、asyncを使って非同期処理を書いてみます。awaitを使うことで実行結果を待つことができます。UIにアクセスする場合は、withContext(Dispatchers.Main)ブロック内に書きます。

ただし、withContext(Dispatchers.Main)の記述がなくても問題なく動きます。また、asyncブロック内の最後の行が戻り値となります。returnはつけてはいけないことになってますので気をつけましょう。

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

suspend関数

コルーチンのasyncの非同期処理を自作の関数へ移したい場合は、suspendを使います。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を使った場合との厳密的な違いはわかっていませんが、suspendwithContextを使ったこちらのほうが、個人的にはわかりやすくて好きです。

var requestCount = 0

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

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

View.onClickを拡張する

こちらのプログラムのように、launchの記述がいちいち面倒だと思うかもしれません。

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

そんな時は、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のまとめ

これまで紹介した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内のコルーチンのキャンセル

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()
    }
}

コルーチンがキャンセルされた時に、例外が投げられますのでフックします。

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

ただし注意点としまして、OkHttpを使った非同期処理を試してみましたが、OkHttpのキャンセルができませんでした。その場合は、OkHttpのCallを呼び出してキャンセルさせましょう。

記事に関するご質問などがあればTwitterへお返事ください。
この記事で紹介した商品
Anrdoidアプリ開発にオススメの書籍

▼ アプリ開発初心者の方は、基本的なことをひととおりおさえられる教科書的な本を、一冊しっかり読んでおくことをオススメします。

▼ こちらは、Kotlinの実践的なコードや書き方が学べます。ある程度Kotlinが使えて、プログラミング技術をさらに磨きたい人向けの内容です。個人的には、センスの良いプログラミングの書き方がたくさん学べて「目からうろこ」の連続だった本です。

デスクワークの負担を減らすアイテム

作業効率をアップさせるには姿勢が大事です!実際に使ってみてどれもオススメなのでよかったら参考に!

関連記事