#42 AndroidでKotlinコルーチンを使う
今回は、Androidアプリで非同期処理を扱う際に使用するKotlinコルーチンについて説明します。
メインスレッドと非同期処理
Androidでは、UIの描画やボタンタップ時の処理などはメインスレッドで実行されます。 もし、サーバーとの通信やデータベース操作といった時間のかかる処理を行ってしまうと、 描画やイベント処理も止まってしまいます。そのため、時間のかかる処理はメインスレッドでは 実行せず、ワーカースレッドを作り、そこで非同期処理を行う必要があります。
Kotlinコルーチンとは、この非同期処理を扱いやすくするための仕組みです。コルーチン自体はKotlin特有のものではなく、 C#やJavaScriptといった言語などでも用意されています。
非同期処理とコールバック地獄
コルーチンのよさを理解するために、まずはコールバックによる非同期処理を見てみましょう。例として次のような処理を考えてみます。
- サーバーからユーザー情報を非同期で取得する
- 取得できたら、その情報を元にユーザーの画像を非同期で取得する
- 画像が取得できたら、UIに反映させる
ユーザー情報取得処理と画像取得処理は非同期処理なので、処理が終わったときに実行したい関数をコールバックの形で渡す必要があります。 次のようなKotlinコードになるでしょう。
fetchUser("user1234") { user ->
fetchImage(user.imageId) { image ->
// imageを使う処理
}
}
非同期処理を行い、結果を待って次の処理を行う場合、このようにどんどんネストが深くなっていきます。 このような状態をコールバック地獄に陥ると呼びます。「callback hell」で検索するとこれよりひどい例がたくさん見つかるでしょう。
コルーチン
コルーチンとは、処理の途中で中断したり、再開したりすることのできる仕組みです。 処理が途中でとまる可能性があるため、コルーチンは通常の関数やメソッドとは違う方法で起動する必要があります。
先程の処理をKotlinコルーチンで書くと、次のようになります。
ここでのfetchUser()
やfetchImage()
はブロッキングで処理を行い、結果を返す関数とします。
GlobalScope.launch {
val user = withContext(Dispatchers.IO) {
fetchUser("user1234")
}
val image = withContext(Dispatchers.IO) {
fetchImage(user.imageId)
}
// imageを使う処理
println(image)
}
launch { }
はコルーチンを起動する関数で、{ }
の中が、コルーチンとして実行される箇所になります。
withContext()
を使うと、実行するスレッドを切り替え、非同期処理とすることができます。
コルーチン内では、非同期処理が途中に入る場合、その処理が終わるのを待ってから次の処理が実行されます。
コルーチンスコープ
先程のlaunch { }
の前にはGlobalScope
がついています。
Kotlinコルーチンの場合、コルーチンを起動するにはコルーチンスコープと呼ばれるオブジェクトが必要です。
コルーチンスコープは、起動したコルーチンを管理し、スコープの破棄とともに起動したコルーチンに対し、キャンセルのメッセージを
自動で発行してくれます。GlobalScope
は、プロセスが生存している間はずっと生存しているコルーチンスコープです。
Androidでコルーチンを使う
コルーチンはライブラリとして提供されています。そのため、build.gradleに依存関係を追加することでAndroidで使えるようになります。 Androidの場合、LifecylceやViewModelのktx版を使用することで、自動でコルーチンのライブラリが追加されます。したがって、build.gradleに 明示的にコルーチンのライブラリを追加する必要はありません。
次の例はフラグメントのktx版をbuild.graldeのdependenciesに追加する例です。
dependencies {
// 中略
implementation 'androidx.fragment:fragment-ktx:1.2.5'
}
fragment-ktx
を追加することで、lifecycle-viewmodel-ktx
もセットで依存関係に追加されます。
そしてlifecycle-viewmodel-ktx
がコルーチンのライブラリを追加してくれます。
ViewModelの中では、次のようにviewModelScope.launch { }
を使ってコルーチンを起動することができます。
この方法で起動したコルーチンは、フラグメントがバックボタンなどで終了し、ViewModelが破棄されるタイミングでキャンセルされます。
class MyViewModel: ViewModel() {
fun fetchImage(userId: String) {
viewModelScope.launch {
// 画像を取得する処理
}
}
}
suspend関数/メソッド
Kotlinでは、コルーチンの中で呼ばれることを想定し、途中で中断や再開が発生する関数/メソッドを定義することができます。 このような関数をsuspend関数と呼びます。
次の例はユーザー取得処理をsuspend関数にしたものです。
コルーチンの中で呼ばれることを想定しているので、withContext
でスレッドの切り替えを行うことができます。
suspend fun fetchUser(id: String): User {
return withContext(Dispatchers.IO) {
// ユーザー情報取得処理
User(id, "image1111")
}
}
コルーチンの中でsuspend関数を呼んだ場合、suspend関数内の処理が終わるまで処理は中断され、処理が終わった時点で続きの処理が実行されます。
GlobalScope.launch {
// fetchUser()とfetchImage()はsuspend関数とする
val user = fetchUser("user1234")
val image = fetchImage(user.imageId)
// imageを使う処理
println(image)
}
まとめ
Kotlinのコルーチンがどのようなものかを簡単に説明し、Androidでの導入方法を紹介しました。 コルーチンによる非同期処理は、Androidアプリ開発における非同期処理のおすすめの方法とされているため、使い方に慣れておきましょう。