😺

【Android】はじめてのCoroutines

2021/03/28に公開

アプリ開発歴( =プログラミング歴)がもうすぐで1年になります
@iTakahiroです。

現在はiOS・Androidの二刀流を目指して日々奮闘中です!
Qiitaでは、Androidの基本的なアーキテクチャであるMVVMに関する記事をシリーズ化して投稿し、1つのごくごく簡単なアプリを更新していくことでアウトプットしています。

これまでの記事 :

ソースコード

今回はMVVMの実装からは少し距離を置き、Coroutinesについて記事を書きます。
本記事では、Coroutines:JetBrains公式ガイド, coroutines:JetBrains公式サンプルプロジェクト の内容を掻い摘んで取り上げています。
公式ドキュメントは英語ですが詳しい手順やメソッドの説明などが記載されているため、もっと詳しい説明が必要な場合は参照してみてください。

また、ご指摘や感想などもコメントいただけると嬉しいです。

アジェンダ

はじめに

Coroutinesとは

Coroutinesとは、Kotlinを開発したJetBrains社によって提供されている非同期処理ライブラリで、軽量な非同期処理を扱います。
非同期処理の実装はGoogleが提供するThreadAsyncTaskなどのクラスで実装することも可能ですが、以下の特徴・理由を考慮し、アーキテクチャ学習用のアプリではCoroutinesを採用しました。

  • 非同期処理ごとにクラスを作成する必要がない。

そのため

  • より安全でエラーが起こりにくい
  • 作成したクラスが残ってしまうことによってリークを起こすことがない
    • ThreadAsyncTaskを使用する場合、作成したクラスからライフサイクルが終了したActivityやViewModel内のメソッドを呼び出されリークするリスクが少なからず存在する
  • コードが簡潔
  • メモリのコストが小さい

Thread, AsyncTaskとは

ThreadとAsyncTaskに関しても、簡単に説明をしておきます。

Threadは、単純に新しいスレッドを作成するクラスです。
そもそもスレッドの概念が分からん! という方はこちらのGoogle公式ドキュメントを参照したり「スレッド」で検索したりしてみてください。
このThreadクラスは単純に別のスレッドを作成するだけのものであるため、処理の順序づけや実行後に値を返す処理を実装する際はコードが煩雑になりがちです。
そこで、これらの不便な点を解消し非同期処理を容易にするために、AsyncTaskというクラスがGoogleにより用意されています。

AsyncTaskは、文字通りAsync(非同期 ⇄ 同期: sync)のTask、つまり非同期処理を実装するためのクラスです。
これにより、容易に非同期処理(処理Aを行う前に処理A'を行っておき、処理Aで出た結果を基にUIスレッド(メインスレッド)で処理Bを実行する、などの処理)を実装することができます。
詳しくは、AsyncTaskを参照してみてください。

導入方法

では早速、Coroutinesを実装していきます。
まずappレベルのbuild.gradleにて、依存関係を記述します。

build.gradle
dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' // 最新版(2020.1.19)
}

また、Projectレベルのbuil.gradleにて、Kotlinのバージョンが最新になっていることを確認します。
最新でなくてもいいと思われますが、バージョンによってはCoroutinesを使用できない場合があります。
Coroutinesを使用できない場合は、sync時やファイルにて各種Coroutinesのコンポーネントをimportするときなどでエラーが出ると思われます(知らんけど)。指定のバージョンに設定するか最新バージョンに書き換え、syncしてみましょう。

build.gradle
buildscript {
    ext.kotlin_version = '1.3.61' // 最新版(2020.1.19)
}

各種キーワード

Coroutinesで提供されているコンポーネントをキーワードごとにまとめます。

CoroutineScope

CoroutineScopeは、すべてのCoroutine Builder(後述するlaunchなど)の土台となるインターフェースです。
このCoroutineScopeを継承したスコープ(GlobalScopeなど)を用いて非同期処理を実装することになります。
例:

fun launchTest() {
    println("処理1")
    GlobalScope.launch {
        println("処理2")  // Thread.sleep(1000)の間に実行される
    }
    Thread.sleep(1000) // 実行中の全スレッドを1秒止める
    println("処理3")
}

結果:

処理1
処理2
処理3

Thread.sleep(1000)を挿入しているのは、GlobalScope.launch内の処理が完了してから処理3を実行・出力させるためです。
このようにスコープ内の処理が完了するまでスコープ外の処理を待機させることはよくあることです。
今回はThread.sleep(1000)により、運良く1000(ms)の内にGlobalScope.launch内の処理が完了しましたが、全ての場合でそうなるとは限りません。
また、無駄な時間も発生するためこの実装は完全ではありません。

でも安心してください。
Coroutinesでは、以下のように完全に処理が完了するのを待ってくれる優しい機能が提供されています!
これらについては後述します。

  • runBlocking
  • withContext

先に述べておくと、withContextはsuspending function(後述します。後述が多くてすみません! )であるため、coroutine内(例えば、GlobalScope.launch{ //ここ})からしか呼ぶことができません。そんな彼らは、coroutine内で処理を止めたいときに使用されます。

(
少し余談ですがCoroutineScopeActivityViewModelなどの明確なLife Cycleを持ったクラス内で実装されるべきです。なぜなら、coroutineをキャンセルするタイミングを明確にすることができるからです。そのタイミングは親のクラスが破棄されるタイミング、ActivityであればonDestroyメソッドなどでよいでしょう。(CoroutineScope).cancel()を呼び出しておけば問題ないと思われます。 おそらくですが、
)

さて、ここで"後述"で先延ばしにしていた説明を行うことにします。

launch

launchは、実行中のスレッドを止めない新たなcoroutineを作成するビルダーで、Jobクラスを返すことができます。

launchは先ほどの例のように、何らかのCoroutineScope(例ではGlobalScope)を基に生成され、ラムダ式({})内の処理が1つのcoroutineとして実行されます。

Jobクラスを返すとはどういうことかというと、launchJobクラスでインスタンス化したり、Jobが提供するcanceljoinなどのメソッドを使用したりできるということです。
また、インスタンス化することで可読性の向上に繋がる可能性もあります(Coroutinesでは軽量な処理が実行されるべきなので、そもそも煩雑な処理は実行しないようにするのが望ましいと思われますが)。

Job

Jobとは、ライフサイクルを持つbackground "job"のことで、Jobが完了したときやcancel()が呼び出されたときにキャンセルされます。
launchなどで生成されます。

ここで、Jobの4つのメソッドの内2つを紹介します。

join()

Jobが完了するまでcoroutineを一時中断してくれる、suspending function(後述)です。
例:

fun launchTest {
    GlobalScope.launch {
        val job = launch {
            println("あ〜あ、ちょっと待ってよぉ〜")
        }
        job.join()
        println("待ったよ!")
    }
    Thread.sleep(1000) // 実行中の全スレッドを1秒止める
    println("終わり")
}

結果:

あ〜あ、ちょっと待ってよぉ〜
待ったよ!
終わり

cancel()

cancelを呼び出すことでJobをキャンセルすることができます。
問題があった場合は、エラーメッセージが出力されます。
例:

GlobalScope.launch {
    val job = launch {
        for (index in 1..10) {
            // 1秒ごとに"あ〜あ、ちょっと待ってよぉ〜"と歌う
            delay(1000)
            println("あ〜あ、ちょっと待ってよぉ〜") 
        }
    }
    // 3秒以上は待てない
    delay(3000)
    job.cancel()
    println("待ちくたびれたよ!")
}

結果:

あ〜あ、ちょっと待ってよぉ〜
あ〜あ、ちょっと待ってよぉ〜
あ〜あ、ちょっと待ってよぉ〜
待ちくたびれたよ!

全文:

fun launchTest() {
    println("始め!")
    GlobalScope.launch {
        val job = launch {
            for (index in 1..10) {
                // 1秒ごとに"あ〜あ、ちょっと待ってよぉ〜"と歌う
                delay(1000)
                println("あ〜あ、ちょっと待ってよぉ〜") 
            }
        }
        // 3秒以上は待てない
        delay(3000)
        job.cancel()
        println("待ちくたびれたよ!")
    }
    Thread.sleep(10000)
    println("終わり!")
}

処理を一時中断する

Kotlinでは非同期処理実装のため、suspending functionという処理をsuspendする(一時停止する)メソッドが提供されています。

また、実行中の処理を一時中断するための機構は以下の2つがあります。

  • runBlocking
  • withContext

runBlockingは、UIスレッドなど実行中のすべての処理を一時中断するために、
withContextは、coroutines内の処理を一時中断するために使用されます。
それらの詳細を以下で説明します。

suspending function

suspending functionとは、このメソッド内の処理が終わるまで実行中の処理を一時停止させることができるようなメソッドです。
Kotlin独自のメソッドのタイプで、Coroutinesを実装する際に使用されます。
suspend funと記述することで定義できます。

一時停止するとはいえ、なんでもかんでも停止できるわけではなく、coroutine内においてのみ停止できます。
そのため、suspend funで定義されたメソッドはcoroutine内(GlobalScope.launch{ //ここ}など)からのみ呼び出すことができます。

runBlocking

runBlockingとは、その名の通り実行中の処理を止める機能です。
ラムダ内の処理が完了するまで、現在実行中のスレッド(coroutineによるものではないUIスレッドなど)を一旦止めることができます。

これはcoroutineと他のスレッドとの橋渡し役となるよう設計されており、coroutine内では使用されません。
例:

fun runBlockingTest() = runBlocking {
    println("処理1")
    GlobalScope.launch {
        println("処理2")  // delay(3000)の間に実行される
    }
//    Thread.sleep(1000) // 実行中の全スレッドを1秒止める
    delay(3000)  // coroutine(ここではrunBlocking)内の処理のみ3秒止める
    println("処理3")
}

実行例:

println("runBlocking、始めるで〜")
runBlockingTest()
println("runBlocking、終わったで〜")

結果:

runBlocking、始めるで〜
処理1
処理2
処理3
runBlocking、終わったで〜

runBlocking内の処理が完了するのをちゃんと待ってくれました!!
優しい!

withContext

withContextは、coroutine内でCoroutineContextというContextを新たに提供し、このwithContext内の処理が完了するまでcoroutine内の処理を一時中断する機能が備わっています。
言うなれば、with "Another" Contextですかね。


引数として指定するCoroutineContextは、CoroutineDispatcherを用いて提供されます。
このCoroutineDispatcherにより、以下3種類のCoroutineContextを実装することができます。

  • Dispatchers.Default:標準のCoroutineContextが提供される。バックグラウンド処理で共通のスレッド・プールを使用するため、CPUを消費するような計算処理を含む場合に最適です。
  • Dispatchers.IO:I/Oの処理を中断させたい場合に使われます。
  • Dispatchers.Unconfined:あまり使われるべきではないようです。

先ほどのrunBlockingTest内のdelayメソッドをwithContextで置き換えた例を記載します。

例:

    @Test
    fun runBlockingTest() = runBlocking {
        println("処理1")
        withContext(Dispatchers.Default) {
            println("処理2")  // Thread.sleep(1000)の間に実行される
        }
//    Thread.sleep(1000) // 実行中の全スレッドを1秒止める
//    delay(3000)  // coroutine(ここではrunBlocking)内の処理のみ3秒止める
        println("処理3")
    }

実行例:

println("runBlocking、始めるで〜")
runBlockingTest()
println("runBlocking、終わったで〜")

結果:

runBlocking、始めるで〜
処理1
処理2
処理3
runBlocking、終わったで〜

この通り、runBlockingTest内の処理(処理1, 2, 3)が順番通りに実行されました!!

まとめ

簡単に概要をまとめます。
Coroutinesとは、軽量なスレッドを提供するKotlin独自の非同期処理ライブラリです。
ラムダ式一つでスレッドを用意し、非同期処理を実装することができます。
runBlockingwithContextなど、実行中のスレッドを一時中断するための機能も簡単に実装することができます。
非同期処理を簡潔に安全に実装できる便利な機構ではないでしょうか。

今回は、Kotlinでの非同期処理で最適と思われるCoroutinesについて記事を書きました。
この情報量でおおよその機能や性質を理解し使用することができるのではないか、と思います。
知らんけど。

参考資料

Discussion