コルーチンとは、Android で使用できる並行実行のデザイン パターンです。これを使用すると、非同期実行するコードを簡略化できます。 コルーチン は、Kotlin にはバージョン 1.3 で追加されたものですが、すでに他の言語で確立されている 概念をベースにしています。
Android では、メインスレッドをブロックしてアプリの応答を止める可能性のある長時間実行タスクの管理に役立ちます。 コルーチンを使用するプロのデベロッパーの 50% 以上が、生産性が向上したと報告しました。このトピックでは、Kotlin コルーチンを使用してこれらの問題に対処する方法を説明し、より簡潔で無駄のないアプリコードを記述できるようにします。
機能
コルーチンは、Android での非同期プログラミングに推奨するソリューションです。主な機能は次のとおりです。
- 軽量: 中断がサポートされているため、1 つのスレッドで多数のコルーチンを実行できます。これにより、コルーチンを実行しているスレッドがブロックされません。中断により、多数の同時実行処理をサポートしつつ、ブロックさせる場合よりメモリを節約できます。
- メモリリークの削減: 構造化された同時実行を使用して、 スコープ内で処理を実行します。
- 組み込みのキャンセル サポート: キャンセル が実行中のコルーチン階層を介して自動的に伝播されます。
- Jetpack の統合: Jetpack ライブラリの多くには、コルーチンを全面的にサポートする 拡張機能が用意されています。一部のライブラリでは、構造化された同時実行に使用できる独自のコルーチン スコープも用意されています。
例の概要
このトピックのサンプルは、アプリ アーキテクチャ ガイドをベースにしており、ネットワーク リクエストを行って、結果をメインスレッドに返し、それをアプリでユーザーに表示するものになっています。
具体的には、ViewModel
アーキテクチャ コンポーネントがメインスレッドのリポジトリ レイヤを呼び出して
ネットワーク リクエストをトリガーします。このガイドでは、コルーチンを使用してメインスレッドのブロックを避けるソリューションについて順を追って説明します。
ViewModel には、コルーチンを直接扱う KTX 拡張機能である `lifecycle-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>() } private const val loginUrl = "https://example.com/login" class LoginRepository(private val responseParser: LoginResponseParser) { // 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で、ViewModelKTX 拡張機能に含まれています。すべてのコルーチンはスコープ内で実行する必要があります。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 コルーチンでアプリのパフォーマンスを改善するをご覧ください。
コルーチンに関するその他の参考情報については、次のリンクをご覧ください。