【Android Studio】Kotlin Coroutinesの使い方!非同期処理とスレッド
この記事からわかること
- Android StudioのKotlin Coroutinesの実装方法の使い方
- 非同期処理を実装する方法
- コルーチンとは?
- runBlockingやlaunch、asyncメソッドの役割
- suspend関数やDispatcherとは?
- Unresolved reference: launchの原因
- Suspend function '関数名' should be called only from a coroutine or another suspend functionの原因
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
公式リファレンス:Kotlin coroutines on Android
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
Kotlin Coroutinesとは?
Kotlin CoroutinesとはAndroidアプリ開発で非同期処理を実装できる公式ライブラリです。また「コルーチン」という言葉自体はAndroidで使用できる並行実行のデザインパターンのことを指しており、日本語に訳すと「特定の機能を担うルーチン」という的な意味になると思います。
Kotlin Coroutinesを使用することでこれまでコールバックなどで実行していたネットワーク処理やデータベースへの書き込み処理(I/O)、また処理に時間のかかるものなどを非同期的に(通常の流れとは異なる流れで)実行することでメインスレッドを活かし、アプリの対話性と生産性の向上を期待することができます。
処理のタイミングが異なる非同期処理を通常の同期処理の間に記述できることで見通しが良く、保守しやすいコードを実装することができます。
用語集
Kotlin Coroutinesを触るにあたって登場する言葉やクラスなどを自分なりにまとめてみました。
- コルーチン:非同期処理
- CoroutineContext:コルーチンの実行に必要な情報を保持
- CoroutineScope:コルーチンを管理するスコープ
- サスペンド関数:コルーチン内で実行できる特別な関数
- Dispatcher:スレッドを指定
- CoroutineBuilder:コルーチンを作成するもの
- Job:コルーチンのライフサイクル、作業の状態を管理
認識がずれていたら申し訳ないですが大体こんなイメージだと思います。
コルーチンとは?
Kotlin Coroutinesでは「コルーチン」という単位で非同期処理が管理されています。非同期処理を実装する場合バックグラウンドスレッドで実行するためコードの実行にラグが生じコールバックなどを使用する必要がありました。しかしKotlin Coroutinesを使用することで非同期処理をより簡潔に記述することが可能になります。また非同期のタスクを一時停止したり再開したりすることができるため、待ち時間のラグやコールバックネストの問題を解決できるようになるようです。
導入方法
Kotlin Coroutinesを使用できるようにするには以下のコードを「build.gradle(Module)」ファイルに記述し「Sync Now」を押します。私はAndroid Studio Flamingo(最新版)を使用していたところ依存関係を追記しなくても使用することができました。Android Studioのバージョンによっては依存関係の追加が必要ないかもしれません。
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
Kotlin Coroutinesを使用した非同期処理
テスト実行環境で試しています。
Kotlin Coroutinesを使用した非同期処理のコードサンプルを見てみます。
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L) // 1秒遅らせる
print("World!")
}
print("Hello")
}
// Hello
// World!
記述されている順番とは逆に出力されているのがわかります。このコードではrunBlocking
とlaunch
がKotlin Coroutinesのコードになります。そしてasync
を含めた3つがよく使うコルーチンを作成するCoroutineBuilderです。
- runBlocking
- launch
- async
runBlocking
expect fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
runBlocking
は現在実行されているスレッドをブロックして処理を実行するコルーチンを作成するCoroutineBuilderです。スレッドをブロックして非同期処理を実行するため、アプリ内で使用するのであればメイン(UI)スレッドでの使用は避ける必要があります。実行してみると中で実行されるのは非同期処理のはずですがコードの流れ通りの処理順になっているのがわかります。
fun main() {
print("1")
runBlocking {
print("2")
}
print("3")
} // 123
launch
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
launch
はメインスレッドをブロックせず(ノンブロッキング)にバックグラウンドで処理を行うコルーチンを作成するCoroutineBuilderです。コルーチンへの参照をJobインスタンスとして返します。そのJobインスタンスには値を含めることができないのでコルーチン内での計算結果などを返すことはできません。またlaunch
はCoroutineScopeでのみ宣言されているためrunBlocking
などのコルーチンの中か明示的にスコープを指定しないと呼び出すことはできません。
fun main() = runBlocking {
launch {
delay(1000L) // 1秒遅らせる
print("World!")
}
print("Hello")
}
明示的にスコープを指定した場合
fun main() {
print("1")
GlobalScope.launch {
print("2")
}
print("3")
} // 213
そのためスコープのない場所で呼び出そうとするとUnresolved reference: launch
というエラーになります。
CoroutineScopeの種類
CoroutineScope
には使用する場所や状況に応じて種類が分けられています。GlobalScope
はアプリが終了するまで存在し続けるので使用はあまり推奨されていません。
- GlobalScope:アプリケーション全体のライフサイクルに関連付けられたスコープ。
- viewModelScope:ViewModel内で使えるスコープ。ViewModelが破棄されるとコルーチンも自動でキャンセル
- lifecycleScope:ActivityやFragment内で使えるスコープ。特定のライフサイクル状態に合わせたコルーチンを実行
async
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
async
はlaunch
同様にメインスレッドをブロックせず(ノンブロッキング)にバックグラウンドで処理を行うコルーチンを作成するCoroutineBuilderです。launch
と異なるのは戻り値を指定できることです。戻ってくる型はJobの1種であるDeferred<T>
型です。またasync
もlaunch
同様にCoroutineScopeでのみ宣言されているためrunBlocking
などのコルーチンの中か明示的にスコープを指定しないと呼び出すことはできません。
まずはサンプルコードをみてみます。これは非同期の取得処理を並列につなげて実行しています。実行してみるとfetchDataFromRemote
とfetchDataFromDatabase
は並列に実行され、開始から5秒後には両方の値が出力されます。fetchDataFromRemote
の2秒待機中にもfetchDataFromDatabase
のカウントが進んでいることになります。
import kotlinx.coroutines.*
fun main() {
runBlocking {
// Deferredオブジェクトを取得
val result1 = async { fetchDataFromRemote() }
val result2 = async { fetchDataFromDatabase() }
// 中身を取得
val remoteData = result1.await()
val databaseData = result2.await()
print("$remoteData")
print("$databaseData")
}
}
suspend fun fetchDataFromRemote(): String {
// 本来ならデータを取得する処理
delay(2000) // 擬似的にディレイで再現
return "リモートサーバーからフェッチしたデータ"
}
suspend fun fetchDataFromDatabase(): String {
// 本来ならデータを取得する処理
delay(5000) // 擬似的にディレイで再現
return "データベースからフェッチしたデータ"
}
async
メソッド内で実行された関数はString
型を返す関数です。ですが戻り型はawait
メソッドを使用してString
のみを取り出します。await
メソッドは非同期処理が完了するまで待機し結果を取得するメソッドです。
suspend関数
suspend
関数はコルーチン内で実行する非同期処理が終了するまで待機して終了できる関数のことです。suspend
キーワードを関数名の前に付与することでsuspend
関数になります。またsuspend
関数は通常の関数内から呼び出そうとするとSuspend function '関数名' should be called only from a coroutine or another suspend function
というエラーが発生するようになっています。
suspend
関数を使用することで非同期処理の完了を待ちつつコールバックなどを使用しない直接的な記法のままコードを記述することができます。
例えばクラウドやローカルからデータを取得するような時間のかかる処理を以下のように返り値として返すことができるようになります。
suspend fun fetchDataFromRemote(): String {
// 本来ならデータを取得する処理
delay(2000) // 擬似的にディレイで再現
return "リモートサーバーからフェッチしたデータ"
}
suspend fun fetchDataFromDatabase(): String {
// 本来ならデータを取得する処理
delay(5000) // 擬似的にディレイで再現
return "データベースからフェッチしたデータ"
}
suspend関数の中ならsuspend関数をそのまま呼び出せる
suspend
関数の中でなら他のsuspend関数をそのまま呼び出すことができます。内部で呼び出した別のsuspend関数も終了するまで待機します。
suspend fun fetchDataFromDatabase(): String {
print("開始")
delay(5000)
print("ディレイ")
val data = fetchDataFromRemote()
print("終了")
return "データベースからフェッチしたデータ"
}
出力されるのは以下の通りになります。
時間 ログ
21:00:00 開始
21:00:05 ディレイ
21:00:07 終了
直列の処理する
並列処理ではなく直列に実行したい場合は以下のようにawait
で待機するタイミングを変更すればOKです。
GlobalScope.launch {
println("開始")
// Deferredオブジェクトを取得
val result1 = async { fetchDataFromRemote() }
val remoteData = result1.await()
println("$remoteData")
val result2 = async { fetchDataFromDatabase() }
val databaseData = result2.await()
println("$databaseData")
}
非同期処理をループで作成して直列に実行する
forループなどで処理を回して非同期処理を生成し、それを直列に実行したい場合は以下のように実装することで実現できます。
- 対象の非同期処理(printNumber)を定義
- 非同期処理を直列で実行するためのメソッド(runSerialTasks)を定義
- 直列で実行する処理を順番に保持する配列(tasks)を用意
- 処理を配列の中に詰めていく
- Taskの中で処理を順番に実行するメソッドを呼び出す
suspend fun printNumber(num: Int): Boolean {
delay(2000) // 2秒待機
println("$num")
return true
}
// 直列に非同期処理を実行する関数
suspend fun runSerialTasks(tasks: List<suspend () -> Boolean>) {
for (task in tasks) {
val success = task()
if (success) {
println("Task completed successfully")
} else {
println("Task failed")
}
}
println("All tasks completed")
}
GlobalScope.launch {
val nums = (1..10).toList()
val tasks = mutableListOf<suspend () -> Boolean>()
for (num in nums) {
tasks.add { printNumber(num) }
}
// 作成したタスクを直列に実行
runSerialTasks(tasks)
}
CoroutineContext
CoroutineContext
コルーチンの実行に必要な情報を保持するオブジェクトです。コルーチンがどのスレッドで実行されるか、エラーハンドリングの方法、タイムアウトの設定など、コルーチンの動作に影響を与えるさまざまな属性をキーと値のペアの集合として保持します。
Dispatcher
Dispatcher
はコルーチンを実行するスレッドを指定するオブジェクトです。この情報はCoroutineContextに保管されます。スレッドを指定するには引数にDispatchersを渡します。
GlobalScope.launch(Dispatchers.IO) {
print("2")
}
Dispatchers.Main
(メインスレッド)やDispatchers.IO
(I/O処理に適したスレッド)などが定義されています。
スレッドの種類
- Dispatchers.Main:メインスレッドで実行。UIを操作の場合のみ使用。LiveDataのアップデートなど
- Dispatchers.IO:メインスレッドの外部でディスクやネットワークのI/Oで実行。RoomのCRUD処理、ファイルの読み書き、APIレスポンス取得など
- Dispatchers.Default:メインスレッドの外部でCPU負荷の高い作業を実行。リストの並べ替えやSONの解析など
withContext
withContext
もメインスレッドをブロックせず(ノンブロッキング)にバックグラウンドで処理を行うコルーチンを作成するCoroutineBuilderです。async/await
と同じような振る舞いをしますが、async/await
がcoroutineを新たに生成する一方、withContext
は実行中のcoroutineのコンテキストを切り替えているだけです。なのでwithContextの方がパフォーマンスがよくメモリ消費も軽減されています。
fun main() {
print("1")
GlobalScope.launch {
withContext(Dispatchers.IO) {
print("2")
}
print("3")
}
print("4")
} // 2314
またsuspend関数を使用する場合以下のように記述することもできます。
val data = withContext(Dispatchers.IO) {
return fetchDataFromRemote()
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。