非同期処理のCoroutine(コルーチン)を始めてみよう!【Android/Kotlin】

そろそろAndroidアプリ開発のKotlinにおいて、非同期処理をCoroutine(コルーチン)でやっていこうと思っている。この記事では、Coroutineの使い方を理解できている範囲でまとめてみた。

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

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を継承するようにする。これにより、onDestroyでコルーチンのJobキャンセルし忘れがなくなり、記述も簡潔に書けるようになる。

class MainActivity : ScopedAppActivity() {
   ...
}

launchを使う

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

launch {
    // メインスレッドで実行される
    var counter = 0
    while (true) {
        textView.text = "${++counter} $message"
        delay(1000) // ブロッキングされない
    }
}
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を使う。

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

Anrdoidアプリ開発にオススメの書籍

こちらの本は、図や絵が多くてわかりやすく、Androidアプリ開発初心者でも理解しやすい内容。基本的なことはしっかりおさえられるので、この手の本は一冊読んでおくと良と思う。

基本からしっかり身につくAndroidアプリ開発入門
基本からしっかり身につくAndroidアプリ開発入門

圧倒的な多数のユーザーが使っているヤフーのアプリ。その制作の最前線にいる黒帯エンジニアが、ユーザーが使いやすいアプリの大切な基本をしっかりと解説します。

KindleAmazon

Kotlinの実践的な使い方が学べる内容の本で、プログラミング技術をもっと磨きたい人向け。センスの良いプログラミングがたくさん紹介されていて、個人的には「目からうろこ」の連続だった本。

Kotlinプログラミング
Kotlinプログラミング

Kotlinは、Javaとの相互運用を可能にし、Android OSでGoogleがフルサポートする静的型プログラミング言語です。この言語は、Javaだけでは十全ではない(Javaだけでは実装に手間がかかりすぎる)、軽量かつ豊かな表現形式や、他言語ではすでに実装されている最新の機能を盛り込んでいます。

KindleAmazon

参考

最後まで読んでいただきありがとうございました。

「この記事が参考になったよ」という方は、ぜひ記事をシェアをしていただけるととても嬉しいです。

今後も有益な記事を書くモチベーションにつながりますので、どうかよろしくお願いいたします。↓↓↓↓↓↓↓

あなたにおすすめ