【Android】はじめてのCoroutines
アプリ開発歴( =プログラミング歴)がもうすぐで1年になります
@iTakahiroです。
現在はiOS・Androidの二刀流を目指して日々奮闘中です!
Qiitaでは、Androidの基本的なアーキテクチャであるMVVMに関する記事をシリーズ化して投稿し、1つのごくごく簡単なアプリを更新していくことでアウトプットしています。
これまでの記事 :
ソースコード
今回はMVVMの実装からは少し距離を置き、Coroutines
について記事を書きます。
本記事では、Coroutines:JetBrains公式ガイド, coroutines:JetBrains公式サンプルプロジェクト の内容を掻い摘んで取り上げています。
公式ドキュメントは英語ですが詳しい手順やメソッドの説明などが記載されているため、もっと詳しい説明が必要な場合は参照してみてください。
また、ご指摘や感想などもコメントいただけると嬉しいです。
アジェンダ
はじめに
Coroutinesとは
Coroutines
とは、Kotlinを開発したJetBrains社によって提供されている非同期処理ライブラリで、軽量な非同期処理を扱います。
非同期処理の実装はGoogleが提供するThreadやAsyncTaskなどのクラスで実装することも可能ですが、以下の特徴・理由を考慮し、アーキテクチャ学習用のアプリではCoroutines
を採用しました。
- 非同期処理ごとにクラスを作成する必要がない。
そのため
- より安全でエラーが起こりにくい
- 作成したクラスが残ってしまうことによってリークを起こすことがない
-
Thread
やAsyncTask
を使用する場合、作成したクラスからライフサイクルが終了した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
にて、依存関係を記述します。
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' // 最新版(2020.1.19)
}
また、Projectレベルのbuil.gradle
にて、Kotlinのバージョンが最新になっていることを確認します。
最新でなくてもいいと思われますが、バージョンによってはCoroutines
を使用できない場合があります。
Coroutines
を使用できない場合は、sync
時やファイルにて各種Coroutines
のコンポーネントをimport
するときなどでエラーが出ると思われます(知らんけど)。指定のバージョンに設定するか最新バージョンに書き換え、sync
してみましょう。
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内で処理を止めたいときに使用されます。
(
少し余談ですがCoroutineScope
はActivity
やViewModel
などの明確なLife Cycleを持ったクラス内で実装されるべきです。なぜなら、coroutineをキャンセルするタイミングを明確にすることができるからです。そのタイミングは親のクラスが破棄されるタイミング、 おそらくですが、Activity
であればonDestroy
メソッドなどでよいでしょう。(CoroutineScope).cancel()
を呼び出しておけば問題ないと思われます。
)
さて、ここで"後述"で先延ばしにしていた説明を行うことにします。
launch
launchは、実行中のスレッドを止めない新たなcoroutineを作成するビルダーで、Job
クラスを返すことができます。
launch
は先ほどの例のように、何らかのCoroutineScope
(例ではGlobalScope
)を基に生成され、ラムダ式({}
)内の処理が1つのcoroutineとして実行されます。
Job
クラスを返すとはどういうことかというと、launch
をJob
クラスでインスタンス化したり、Job
が提供するcancel
やjoin
などのメソッドを使用したりできるということです。
また、インスタンス化することで可読性の向上に繋がる可能性もあります(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独自の非同期処理ライブラリです。
ラムダ式一つでスレッドを用意し、非同期処理を実装することができます。
runBlocking
やwithContext
など、実行中のスレッドを一時中断するための機能も簡単に実装することができます。
非同期処理を簡潔に安全に実装できる便利な機構ではないでしょうか。
今回は、Kotlinでの非同期処理で最適と思われるCoroutines
について記事を書きました。
この情報量でおおよその機能や性質を理解し使用することができるのではないか、と思います。
知らんけど。
参考資料
- Coroutines:JetBrains公式ガイド
- coroutines:JetBrains公式サンプルプロジェクト
- Thread:Google公式リファレンス
- AsyncTask:Google公式リファレンス
- CoroutineScope:JetBrains公式リファレンス
- launch:JetBrains公式リファレンス
- Job:JetBrains公式リファレンス
- CoroutineDispatcher:JetBrains公式リファレンス
Discussion