【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]
\ アプリをリリースしました /
公式リファレンス: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
runBlocking
は現在実行されているスレッドをブロックして処理を実行するコルーチンを作成するCoroutineBuilderです。スレッドをブロックして非同期処理を実行するため、アプリ内で使用するのであればUIスレッドなどのメインスレッドで使用することは避ける必要があります。実行してみると中で実行されるのは非同期処理ですがコードの流れ通りの処理順になっているのがわかります。
fun main() {
print("1")
runBlocking {
print("2")
}
print("3")
} // 123
launch
launch
はメインスレッドをブロックせず(ノンブロッキング)にバックグラウンドで処理を行うコルーチンを作成するCoroutineBuilderです。またlaunch
は戻り値を返さないのでコルーチン内での計算結果を取得することはできません。またこの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
async
はlaunch
同様にメインスレッドをブロックせず(ノンブロッキング)にバックグラウンドで処理を行うコルーチンを作成するCoroutineBuilderです。launch
と異なるのは戻り値を指定できることです。戻ってくる型はDeferred<T>
です。
まずはサンプルコードをみてみます。
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(1500)
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 fun fetchDataFromRemote(): String {
delay(2000)
return "リモートサーバーからフェッチ"
}
suspend fun fetchDataFromDatabase(): String {
delay(1500)
return "データベースからフェッチ"
}
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
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
個人開発に限界を感じたらiOSに特化したプログラミングスクール「iOSアカデミア」も検討してみてください!無料相談可能で「最短・最速」でiOSエンジニアになれるように手助けしてくれます。