Kotlinでコルーチンをはじめよう【Androidアプリ開発】

Kotlinでコルーチンをはじめよう【Androidアプリ開発】
Kotlinでコルーチンをはじめよう【Androidアプリ開発】

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

▼ こちらの書籍でもCoroutineの使い方が解説されてます。

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

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

コルーチン
コルーチン

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

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

ScopedAppActivityクラスの作成

こちらの公式ドキュメントを参考にプログラムを解説していきます。 kotlinx.coroutines/coroutines-guide-ui.md · GitHub Basics - Kotlin Programming Language

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

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

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

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

kotlin
class MainActivity : ScopedAppActivity() {
   ...
}

launchを使う

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

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

また、delayはコルーチンを中断する関数です。プログラム内容を覗くと、次のようなsuspendで定義されてます。

kotlin
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はつけてはいけないことになってますので気をつけましょう。

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

suspend関数

コルーチンのasyncの非同期処理を自作の関数へ移したい場合は、suspendを使います。suspendを使えば、次のように非同期処理を分離でき、可読性が上がります。

kotlin
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を使ったこちらのほうが、個人的にはわかりやすくて好きです。

kotlin
var requestCount = 0

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

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

View.onClickを拡張する

こちらのプログラムのように、launchの記述がいちいち面倒です。

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

そんな時は、View.onClickを拡張しちゃいましょう。

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

すると、次のようにイベントハンドラ内がシンプルになりました。

kotlin
button.onClick {
    message = fetchIOData()
}

詳しくは公式ドキュメントをご覧ください。 kotlinx.coroutines/coroutines-guide-ui.md · GitHub

ここまでのMainActivityのまとめ

これまで紹介したMainActivityのプログラムをまとめてみます。

kotlin
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を呼び出することでコルーチンをキャンセルできます。
kotlin
object EchoApiGenerator {
    var mTask:CoroutineScope? = null

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

        ...非同期処理
    }

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

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

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

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

▼ こんな記事も書いてます。

関連記事

 
最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

関連記事