コルーチンとは、Android で使用できる並行実行のデザイン パターンです。これを使用すると、非同期実行するコードを簡略化できます。コルーチン は、バージョン 1.3 で Kotlin に追加され、確立された コンセプトを学習できます。
Android では、メインスレッドをブロックしてアプリの応答を止める可能性のある長時間実行タスクの管理に役立ちます。コルーチンを使用するプロのデベロッパーの 50% 以上が、生産性が向上したと報告しました。このトピックでは、Kotlin コルーチンを使用してこれらの問題に対処する方法を説明し、より簡潔で無駄のないアプリコードを記述できるようにします。
機能
コルーチンは、Android での非同期プログラミングに推奨するソリューションです。主な機能は次のとおりです。
- 軽量: 中断がサポートされているため、1 つのスレッドで多数のコルーチンを実行できます。これにより、コルーチンを実行しているスレッドがブロックされません。中断により、多数の同時実行処理をサポートしつつ、ブロックさせる場合よりメモリを節約できます。
- メモリリークが少ない: 構造化された同時実行 スコープ内でオペレーションを実行できます
- 組み込みの解約サポート: 解約 実行中のコルーチン階層を通じて自動的に伝播されます。
- Jetpack の統合: Jetpack ライブラリの多くには、コルーチンを全面的にサポートする拡張機能が用意されています。一部のライブラリでは、構造化された同時実行に使用できる独自のコルーチン スコープも用意されています。
例の概要
このトピックのサンプルは、アプリ アーキテクチャ ガイドをベースにしており、ネットワーク リクエストを行って、結果をメインスレッドに返し、それをアプリでユーザーに表示するものになっています。
具体的には、ViewModel
アーキテクチャ コンポーネントがメインスレッドのリポジトリ レイヤを呼び出してネットワーク リクエストをトリガーします。このガイドでは、さまざまなソリューションについて説明します。
コルーチンを使用してメインスレッドのブロックを解除します。
ViewModel
には、コルーチンを直接扱う KTX 拡張機能であるlifecycle-viewmodel-ktx
ライブラリが用意されており、このガイドでも使用されています。
依存関係情報
Android プロジェクトでコルーチンを使用するには、アプリの build.gradle
ファイルに次の依存関係を追加します。
Groovy
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
バックグラウンド スレッドでの実行
メインスレッドでネットワーク リクエストを行うと、レスポンスを受信するまでの間、待機したり、ブロックされたりすることになります。スレッドがブロックされるため、OS は onDraw()
を呼び出せず、アプリがフリーズして、アプリケーション応答なし(ANR)のダイアログが表示される可能性があります。ユーザー エクスペリエンスを向上させるために、この処理をバックグラウンド スレッドで実行するようにしましょう。
まず、Repository
クラスを見て、ネットワーク リクエストがどのように行われているかを確認します。
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
class LoginRepository(private val responseParser: LoginResponseParser) {
private const val loginUrl = "https://example.com/login"
// Function that makes the network request, blocking the current thread
fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
val url = URL(loginUrl)
(url.openConnection() as? HttpURLConnection)?.run {
requestMethod = "POST"
setRequestProperty("Content-Type", "application/json; utf-8")
setRequestProperty("Accept", "application/json")
doOutput = true
outputStream.write(jsonBody.toByteArray())
return Result.Success(responseParser.parse(inputStream))
}
return Result.Error(Exception("Cannot open HttpURLConnection"))
}
}
makeLoginRequest
は同期しており、呼び出し元のスレッドをブロックします。ネットワーク リクエストのレスポンスをモデル化するために、独自の Result
クラスが用意されています。
ViewModel
は、ボタンなどがクリックされたときにネットワーク リクエストをトリガーします。
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
前のコードでは、ネットワーク リクエストを行うときに、LoginViewModel
が UI スレッドをブロックしています。この実行をメインスレッドから移動させるには、新しいコルーチンを作成して、I/O スレッドでネットワーク リクエストを実行するのが最も簡単です。
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine to move the execution off the UI thread
viewModelScope.launch(Dispatchers.IO) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
}
login
関数のコルーチン コードを詳しく見てみましょう。
viewModelScope
は定義済みのCoroutineScope
で、ViewModel
KTX 拡張機能に含まれています。すべてのコルーチンはスコープ内で実行する必要があります。CoroutineScope
により、1 つ以上の関連するコルーチンを管理します。launch
は、コルーチンを作成し、この関数自体の実行内容を対応するディスパッチャーにディスパッチする関数です。Dispatchers.IO
は、このコルーチンが I/O 処理用に予約されたスレッドで実行されることを示します。
login
関数は次のように実行されます。
- アプリが、メインスレッドで
View
レイヤからlogin
関数を呼び出します。 launch
が新しいコルーチンを作成し、I/O 処理用に予約されたスレッドで独自にネットワーク リクエストが行われます。- このコルーチンの実行中も、
login
関数は実行を継続し、場合によってはネットワーク リクエストが完了する前に戻ります。簡単にするため、ここではネットワーク レスポンスを無視します。
このコルーチンは viewModelScope
で開始されるため、ViewModel
のスコープで実行されます。ユーザーが画面から移動して ViewModel
が破棄された場合、viewModelScope
は自動的にキャンセルされ、実行中のすべてのコルーチンもキャンセルされます。
前のサンプルには、makeLoginRequest
を呼び出すときに、実行を明示的にメインスレッドから移動させる必要があるという問題があります。Repository
を変更して、この問題を解決する方法を見てみましょう。
コルーチンでメインセーフティにする
ある関数がメインスレッドでの UI の更新をブロックしないとき、その関数はメインセーフティであると言います。メインスレッドから makeLoginRequest
関数を呼び出すと UI がブロックされるため、makeLoginRequest
はメインセーフティではありません。コルーチンの実行を別のスレッドに移動するために、次のようにコルーチン ライブラリの withContext()
関数を使用します。
class LoginRepository(...) {
...
suspend fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
// Move the execution of the coroutine to the I/O dispatcher
return withContext(Dispatchers.IO) {
// Blocking network request code
}
}
}
withContext(Dispatchers.IO)
は、コルーチンの実行を I/O スレッドに移動し、呼び出し元の関数をメインセーフティにして、必要なときに UI が更新されるようにします。
makeLoginRequest
には suspend
キーワードも付いています。このキーワードは、Kotlin 独自のもので、関数がコルーチン内から呼び出されるようにするものです。
次のサンプルでは、LoginViewModel
内でコルーチンが作成されます。makeLoginRequest
の実行がメインスレッドの外に移り、login
関数のコルーチンがメインスレッドで実行されるようになります。
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
// Make the network call and suspend execution until it finishes
val result = loginRepository.makeLoginRequest(jsonBody)
// Display result of the network request to the user
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
makeLoginRequest
は suspend
関数であり、すべての suspend
関数はコルーチンで実行する必要があるため、ここでは引き続きコルーチンが必要です。
このコードには、前の login
のサンプルとは異なる点があります。
launch
にDispatchers.IO
パラメータがない。launch
にDispatcher
を渡さない場合、viewModelScope
から起動されたコルーチンがすべてメインスレッドで実行されます。- ネットワーク リクエストの結果が処理され、成功または失敗の UI が表示される。
login 関数は次のように実行されます。
- アプリが、メインスレッドで
View
レイヤからlogin()
関数を呼び出します。 launch
がメインスレッドで新しいコルーチンを作成し、そのコルーチンの実行が開始されます。- コルーチン内では、
loginRepository.makeLoginRequest()
を呼び出すと、makeLoginRequest()
内のwithContext
ブロックの実行が終了するまでコルーチンの実行を中断するようになりました。 withContext
ブロックが終了すると、login()
のコルーチンはメインスレッドで実行を再開し、ネットワーク リクエストの結果を返します。
例外の処理
Repository
レイヤがスローする例外を処理するには、Kotlin の例外に対する組み込みサポートを使用します。次のサンプルでは、try-catch
ブロックを使用しています。
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
viewModelScope.launch {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
val result = try {
loginRepository.makeLoginRequest(jsonBody)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
このサンプルでは、makeLoginRequest()
の呼び出しでスローされた予期しない例外は、UI でエラーとして処理されます。
コルーチンに関する参考情報
Android でのコルーチンの詳細については、Kotlin コルーチンでアプリのパフォーマンスを改善するをご覧ください。
コルーチンに関するその他の参考情報については、次のリンクをご覧ください。