この Codelab では、Android アプリで Kotlin コルーチンを使用する方法を学びます。これは、コールバックの必要性を減らすことでコードを簡素化できる、バックグラウンド スレッドを管理する新しい方法です。コルーチンは、データベースやネットワーク アクセスなどの長時間実行タスクの非同期コールバックを順次コードに変換する Kotlin の機能です。
演習内容を理解しやすいように、コード スニペットを紹介します。
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
コールバック ベースのコードは、コルーチンを使用して順次コードに変換されます。
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
まず、アーキテクチャ コンポーネントを使用して作成された既存のアプリについて説明します。このアプリは、長時間実行されるタスクにコールバック スタイルを使用します。
この Codelab を修了すると、アプリでコルーチンを使用してネットワークからデータを読み込むのに十分な経験を積み、コルーチンをアプリに統合できるようになります。また、コルーチンのベスト プラクティスと、コルーチンを使用するコードに対するテストの作成方法についても理解できます。
前提条件
- アーキテクチャ コンポーネント
ViewModel
、LiveData
、Repository
、Room
に精通している。 - 拡張関数やラムダを含む Kotlin 構文の使用経験
- メインスレッド、バックグラウンド スレッド、コールバックなど、Android でのスレッドの使用に関する基本的な知識
演習内容
- コルーチンで記述されたコードを呼び出し、結果を取得します。
- suspend 関数を使用して非同期コードを順次処理します。
launch
とrunBlocking
を使用して、コードの実行方法を制御します。suspendCoroutine
を使用して既存の API をコルーチンに変換する手法について学習します。- アーキテクチャ コンポーネントでコルーチンを使用する。
- コルーチンのテストのベスト プラクティスを学習します。
必要なもの
- Android Studio 3.5(Codelab は他のバージョンでも動作する可能性がありますが、一部が欠けたり、外観が異なったりすることがあります)。
この Codelab で問題(コードのバグ、文法的な誤り、不明確な表現など)が見つかった場合は、Codelab の左下隅にある [誤りを報告] から問題を報告してください。
コードをダウンロードする
次のリンクをクリックして、この Codelab 用のコードすべてをダウンロードします。
または次のコマンドを使用して、コマンドラインから GitHub リポジトリのクローンを作成します。
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
よくある 質問
まず、サンプルアプリがどんなものか見てみましょう。次の手順に沿って、Android Studio でサンプルアプリを開いてください。
kotlin-coroutines
zip ファイルをダウンロードした場合は、ファイルを解凍します。- Android Studio で
coroutines-codelab
プロジェクトを開きます。 start
アプリケーション モジュールを選択します。[Run] ボタンをクリックして、エミュレータを選択するか、Android Lollipop が動作する Android デバイスを接続します(対応 SDK は 21 以降)。Kotlin コルーチン画面が表示されます。
このスターター アプリでは、画面をタップしてから少し遅れてカウントを増やすためにスレッドを使用します。また、ネットワークから新しいタイトルを取得して画面に表示します。今すぐ試してみると、少し遅れてカウントとメッセージが変化します。この Codelab では、このアプリケーションをコルーチンを使用するように変換します。
このアプリは、アーキテクチャ コンポーネントを使用して、MainActivity
の UI コードを MainViewModel
のアプリケーション ロジックから分離します。このプロジェクトの構造をよく理解しておいてください。
MainActivity
は UI を表示し、クリック リスナーを登録し、Snackbar
を表示できます。イベントをMainViewModel
に渡し、MainViewModel
のLiveData
に基づいて画面を更新します。MainViewModel
はonMainViewClicked
のイベントを処理し、LiveData.
を使用してMainActivity
と通信します。Executors
は、バックグラウンド スレッドで処理を実行できるBACKGROUND,
を定義します。TitleRepository
はネットワークから結果を取得し、データベースに保存します。
プロジェクトにコルーチンを追加する
Kotlin でコルーチンを使用するには、プロジェクトの build.gradle (Module: app)
ファイルに coroutines-core
ライブラリを含める必要があります。Codelab プロジェクトではすでにこの処理が行われているため、Codelab を完了するためにこの処理を行う必要はありません。
Android のコルーチンは、コア ライブラリと Android 固有の拡張機能として利用できます。
- kotlinx-corountines-core — Kotlin でコルーチンを使用するためのメイン インターフェース
- kotlinx-coroutines-android — コルーチンでの Android メインスレッドのサポート
スターター アプリには、build.gradle.
に依存関係がすでに含まれています。新しいアプリ プロジェクトを作成する場合は、build.gradle (Module: app)
を開いて、コルーチンの依存関係をプロジェクトに追加する必要があります。
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
Android では、メインスレッドのブロックを回避することが重要です。メインスレッドは、UI の更新をすべて処理する単一のスレッドです。このスレッドは、すべてのクリック ハンドラとその他の UI コールバックを呼び出すスレッドでもあります。そのため、スムーズなユーザー エクスペリエンスを確保するには、スムーズに実行する必要があります。
ユーザーが視認できるような途切れがなくアプリを表示させるには、メインスレッドは 16 ミリ秒以上ごとに、つまり 1 秒あたり約 60 フレームで画面を更新する必要があります。サイズの大きい JSON データセットの解析、データベースへのデータの書き込み、ネットワークからのデータの取得など、多くの一般的なタスクにはこれよりも長い時間がかかります。したがって、メインスレッドからこのようなコードを呼び出すと、アプリの一時停止、途切れ、またはフリーズが発生する可能性があります。メインスレッドを長時間ブロックすると、アプリがクラッシュして、アプリケーション応答なしのダイアログが表示される場合もあります。
次の動画では、コルーチンがメインセーフティを導入することで、Android でこの問題を解決する方法について説明しています。
コールバック パターン
メインスレッドをブロックせずに長時間実行タスクを実行するためのパターンの 1 つがコールバックです。コールバックを使用すると、長時間実行されるタスクをバックグラウンド スレッドで開始できます。タスクが完了すると、コールバックが呼び出され、メインスレッドで結果が通知されます。
コールバック パターンの例を見てみましょう。
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
このコードは @UiThread
でアノテーションが付けられているため、メインスレッドで実行できるほど高速に実行する必要があります。つまり、次の画面更新が遅れないように、非常に迅速に返す必要があります。ただし、slowFetch
の完了には数秒から数分かかるため、メインスレッドは結果を待つことができません。show(result)
コールバックを使用すると、slowFetch
をバックグラウンド スレッドで実行し、準備ができたら結果を返すことができます。
コルーチンを使用してコールバックを削除する
コールバックは優れたパターンですが、デメリットもあります。コールバックを多用するコードは読み取りと推測が困難になる可能性があります。また、コールバックでは、一部の言語機能(例外など)を使用できません。
Kotlin コルーチンを使用すると、コールバック ベースのコードを順次コードに変換できます。通常、コードを順番に記述すると読みやすくなり、例外などの言語機能を使用できるようになります。
最終的に、コルーチンとコールバックでも同様に同じタスクが実行されます。長時間実行タスクが結果を取得するのを待ってから、タスクの実行を続行します。ただし、コードでは大きく異なります。
キーワード suspend
は、コルーチンで使用できる関数または関数型をマークする Kotlin 独自の方法です。コルーチンが suspend
とマークされた関数を呼び出すと、通常の関数呼び出しのようにその関数が戻るまでブロックするのではなく、結果が準備できるまで実行を一時停止し、結果を使用して中断したところから再開します。結果を待機して中断している間、実行中のスレッドのブロックを解除 し、他の関数やコルーチンを実行できるようにします。
たとえば、次のコードでは、makeNetworkRequest()
と slowFetch()
はどちらも suspend
関数です。
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
コールバック バージョンと同様に、makeNetworkRequest
は @UiThread
とマークされているため、メインスレッドからすぐに戻る必要があります。つまり、通常は slowFetch
などのブロッキング メソッドを呼び出すことはできません。ここで suspend
キーワードが機能します。
コールバック ベースのコードと比較して、コルーチン コードはより少ないコードで現在のスレッドのブロック解除という同じ結果を実現します。シーケンシャル スタイルなので、複数のコールバックを作成せずに、複数の長時間実行タスクを簡単にチェーンできます。たとえば、2 つのネットワーク エンドポイントから結果を取得してデータベースに保存するコードは、コールバックのないコルーチンの関数として記述できます。次に例を示します。
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
次のセクションでは、サンプルアプリにコルーチンを導入します。
この演習では、遅延後にメッセージを表示するコルーチンを作成します。まず、Android Studio でモジュール start
が開いていることを確認します。
CoroutineScope について
Kotlin では、すべてのコルーチンが CoroutineScope
内で実行されます。スコープは、そのジョブを通じてコルーチンの存続期間を制御します。スコープのジョブをキャンセルすると、そのスコープで開始されたコルーチンがすべてキャンセルされます。Android では、たとえばユーザーが Activity
または Fragment
から移動したときに、スコープを使用して実行中のすべてのコルーチンをキャンセルできます。スコープでは、デフォルトのディスパッチャーを指定することもできます。ディスパッチャは、コルーチンを実行するスレッドを制御します。
UI によって開始されたコルーチンは、通常、Android のメインスレッドである Dispatchers.Main
で開始するのが適切です。Dispatchers.Main
で開始されたコルーチンは、一時停止中にメインスレッドをブロックしません。ViewModel
コルーチンはほぼ常にメインスレッドで UI を更新するため、メインスレッドでコルーチンを開始すると、余分なスレッド切り替えを回避できます。メインスレッドで開始されたコルーチンは、開始後にいつでもディスパッチャを切り替えることができます。たとえば、別のディスパッチャーを使用して、メインスレッドから大きな JSON 結果を解析できます。
viewModelScope の使用
AndroidX lifecycle-viewmodel-ktx
ライブラリは、UI 関連のコルーチンを開始するように構成された CoroutineScope を ViewModels に追加します。このライブラリを使用するには、プロジェクトの build.gradle (Module: start)
ファイルに含める必要があります。この手順は、Codelab プロジェクトですでに完了しています。
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
このライブラリは、ViewModel
クラスの拡張関数として viewModelScope
を追加します。このスコープは Dispatchers.Main
にバインドされ、ViewModel
がクリアされると自動的にキャンセルされます。
スレッドからコルーチンに切り替える
MainViewModel.kt
で、次の TODO と次のコードを見つけます。
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
このコードでは、BACKGROUND ExecutorService
(util/Executor.kt
で定義)を使用してバックグラウンド スレッドで実行します。sleep
は現在のスレッドをブロックするため、メインスレッドで呼び出されると UI がフリーズします。ユーザーがメインビューをクリックしてから 1 秒後に、スナックバーがリクエストされます。
コードから BACKGROUND を削除して再度実行すると、この処理を確認できます。読み込みスピナーは表示されず、1 秒後にすべてが最終状態に「ジャンプ」します。
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
updateTaps
を、同じ処理を行うコルーチン ベースのコードに置き換えます。launch
と delay
をインポートする必要があります。
MainViewModel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
このコードも同じ処理を行い、1 秒待ってからスナックバーを表示します。ただし、以下のような重要な違いがあります。
viewModelScope.
launch
はviewModelScope
でコルーチンを開始します。つまり、viewModelScope
に渡したジョブがキャンセルされると、このジョブ/スコープ内のすべてのコルーチンがキャンセルされます。ユーザーがdelay
が戻る前にアクティビティを離れた場合、ViewModel の破棄時にonCleared
が呼び出されると、このコルーチンは自動的にキャンセルされます。viewModelScope
のデフォルトのディスパッチャはDispatchers.Main
であるため、このコルーチンはメインスレッドで起動されます。さまざまなスレッドの使用方法については、後で説明します。- 関数
delay
はsuspend
関数です。これは、Android Studio の左側のガターにあるアイコンで示されます。このコルーチンはメインスレッドで実行されますが、
delay
はスレッドを 1 秒間ブロックしません。代わりに、ディスパッチャーは、次のステートメントで 1 秒後に再開するようにコルーチンをスケジュールします。
実行してみましょう。メインビューをクリックすると、1 秒後にスナックバーが表示されます。
次のセクションでは、この関数をテストする方法について説明します。
この演習では、先ほど作成したコードのテストを記述します。この演習では、 kotlinx-coroutines-test ライブラリを使用して Dispatchers.Main
で実行されているコルーチンをテストする方法について説明します。この Codelab の後半では、コルーチンを直接操作するテストを実装します。
既存のコードを確認する
androidTest
フォルダの MainViewModelTest.kt
を開きます。
MainViewModelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
ルールは、JUnit でテストの実行前後にコードを実行する方法です。オフデバイス テストで MainViewModel をテストできるように、次の 2 つのルールが使用されます。
InstantTaskExecutorRule
は、各タスクを同期的に実行するようにLiveData
を構成する JUnit ルールです。MainCoroutineScopeRule
は、このコードベースのカスタムルールで、kotlinx-coroutines-test
のTestCoroutineDispatcher
を使用するようにDispatchers.Main
を構成します。これにより、テストでテスト用の仮想クロックを進めることができ、コードで単体テストでDispatchers.Main
を使用できるようになります。
setup
メソッドでは、テスト用のフェイクを使用して MainViewModel
の新しいインスタンスが作成されます。これらは、実際のネットワークやデータベースを使用せずにテストを作成できるように、スターター コードで提供されるネットワークとデータベースのフェイク実装です。
このテストでは、MainViewModel
の依存関係を満たすためにのみフェイクが必要です。このコードラボの後半で、コルーチンをサポートするようにフェイクを更新します。
コルーチンを制御するテストを作成する
メインビューがクリックされてから 1 秒後にタップが更新されることを確認する新しいテストを追加します。
MainViewModelTest.kt
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
onMainViewClicked
を呼び出すと、作成したコルーチンが起動します。このテストでは、onMainViewClicked
が呼び出された直後にタップテキストが「0 taps」のままであり、1 秒後に「1 taps」に更新されることを確認します。
このテストでは、仮想時間を使用して、onMainViewClicked
によって起動されたコルーチンの実行を制御します。MainCoroutineScopeRule
を使用すると、Dispatchers.Main
で起動されたコルーチンの実行を一時停止、再開、制御できます。ここでは advanceTimeBy(1_000)
を呼び出しています。これにより、メイン ディスパッチャは 1 秒後に再開するようにスケジュールされたコルーチンを直ちに実行します。
このテストは完全に決定論的であるため、常に同じ方法で実行されます。また、Dispatchers.Main
で起動されたコルーチンの実行を完全に制御できるため、値が設定されるまで 1 秒間待つ必要はありません。
既存のテストを実行する
- エディタでクラス名
MainViewModelTest
を右クリックして、コンテキスト メニューを開きます。 - コンテキスト メニューで、
Run 'MainViewModelTest' を選択します。
- 以降の実行では、ツールバーの
ボタンの横にある構成でこのテスト構成を選択できます。デフォルトでは、構成は MainViewModelTest と呼ばれます。
テストに合格したことが表示されます。実行には 1 秒もかかりません。
次の演習では、既存のコールバック API からコルーチンを使用するように変換する方法を学びます。
このステップでは、コルーチンを使用するようにリポジトリの変換を開始します。そのため、ViewModel
、Repository
、Room
、Retrofit
にコルーチンを追加します。
コルーチンを使用するように切り替える前に、アーキテクチャの各部分が何を担当しているかを理解しておくことをおすすめします。
MainDatabase
は、Title
を保存して読み込む Room を使用してデータベースを実装します。MainNetwork
は、新しいタイトルを取得するネットワーク API を実装します。Retrofit を使用してタイトルを取得します。Retrofit
は、エラーまたはモックデータをランダムに返すように構成されていますが、それ以外の場合は、実際にはネットワーク リクエストを行っているかのように動作します。TitleRepository
は、ネットワークとデータベースのデータを組み合わせてタイトルを取得または更新するための単一の API を実装します。MainViewModel
は画面の状態を表し、イベントを処理します。これにより、ユーザーが画面をタップしたときにリポジトリにタイトルの更新を指示します。
ネットワーク リクエストは UI イベントによって駆動され、それに基づいてコルーチンを開始する必要があるため、コルーチンの使用を開始する自然な場所は ViewModel
です。
コールバック バージョン
MainViewModel.kt
を開いて、refreshTitle
の宣言を確認します。
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
この関数は、ユーザーが画面をクリックするたびに呼び出されます。これにより、リポジトリがタイトルを更新し、新しいタイトルをデータベースに書き込みます。
この実装では、コールバックを使用して次の処理を行います。
- クエリを開始する前に、
_spinner.value = true
の読み込みスピナーが表示されます。 - 結果を取得すると、
_spinner.value = false
で読み込みスピナーをクリアします。 - エラーが発生した場合は、スナックバーを表示するよう指示し、スピナーをクリアします。
onCompleted
コールバックには title
が渡されないことに注意してください。すべてのタイトルを Room
データベースに書き込むため、UI は Room
によって更新される LiveData
を監視して、現在のタイトルに更新されます。
コルーチンの更新では、まったく同じ動作を維持します。Room
データベースなどのオブザーバブル データソースを使用して、UI を自動的に最新の状態に保つのが良いパターンです。
コルーチン バージョン
コルーチンを使用して refreshTitle
を書き直しましょう。
すぐに必要になるため、リポジトリに空の suspend 関数(TitleRespository.kt
)を作成しましょう。suspend
演算子を使用して、コルーチンで動作することを Kotlin に伝える新しい関数を定義します。
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
この Codelab が完了したら、Retrofit と Room を使用して新しいタイトルを取得し、コルーチンを使用してデータベースに書き込むように更新します。今のところ、500 ミリ秒間だけ作業をしているふりをして、その後続行します。
MainViewModel
で、refreshTitle
のコールバック バージョンを新しいコルーチンを起動するバージョンに置き換えます。
MainViewModel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
この関数をステップ実行してみましょう。
viewModelScope.launch {
タップ数を更新するコルーチンと同様に、まず viewModelScope
で新しいコルーチンを起動します。この場合、Dispatchers.Main
が使用されますが、問題ありません。refreshTitle
はネットワーク リクエストとデータベース クエリを実行しますが、コルーチンを使用して メインセーフ インターフェースを公開できます。つまり、メインスレッドから安全に呼び出すことができます。
viewModelScope
を使用しているため、ユーザーがこの画面から移動すると、このコルーチンによって開始された処理は自動的にキャンセルされます。つまり、余分なネットワーク リクエストやデータベース クエリは行われません。
次の数行のコードは、実際には repository
の refreshTitle
を呼び出します。
try {
_spinner.value = true
repository.refreshTitle()
}
このコルーチンは、処理を行う前に読み込みスピナーを開始し、通常の関数と同様に refreshTitle
を呼び出します。ただし、refreshTitle
は中断関数であるため、通常の関数とは異なる方法で実行されます。
コールバックを渡す必要はありません。コルーチンは、refreshTitle
によって再開されるまで一時停止します。通常のブロッキング関数呼び出しとまったく同じように見えますが、ネットワークとデータベースのクエリが完了するまで自動的に待機し、メインスレッドをブロックせずに再開します。
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
中断関数の例外は、通常の関数のエラーと同様に機能します。suspend 関数でエラーをスローすると、呼び出し元にスローされます。実行方法は大きく異なりますが、通常の try/catch ブロックを使用して処理できます。これは、すべてのコールバックに対してカスタム エラー処理を構築するのではなく、エラー処理に組み込みの言語サポートを利用できるため便利です。
また、コルーチンから例外をスローすると、そのコルーチンはデフォルトで親をキャンセルします。つまり、関連する複数のタスクをまとめて簡単にキャンセルできます。
最後に、finally ブロックで、クエリの実行後にスピナーが常にオフになるようにします。
開始構成を選択して を押してアプリケーションを再度実行すると、任意の場所をタップしたときに読み込みスピナーが表示されます。ネットワークやデータベースをまだ接続していないため、タイトルは変わりません。
次の演習では、リポジトリを更新して実際に処理を行います。
この演習では、コルーチンが実行されるスレッドを切り替えて、TitleRepository
の動作バージョンを実装する方法を学習します。
refreshTitle の既存のコールバック コードを確認する
TitleRepository.kt
を開き、既存のコールバック ベースの実装を確認します。
TitleRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
TitleRepository.kt
では、読み込みとエラーの状態を呼び出し元に伝えるコールバックを使用して、メソッド refreshTitleWithCallbacks
が実装されています。
この関数は、更新を実装するために多くの処理を行います。
BACKGROUND
ExecutorService
で別のスレッドに切り替える- ブロッキング
execute()
メソッドを使用してfetchNextTitle
ネットワーク リクエストを実行します。これにより、現在のスレッド(この場合はBACKGROUND
のスレッドの 1 つ)でネットワーク リクエストが実行されます。 - 結果が成功した場合は、
insertTitle
でデータベースに保存し、onCompleted()
メソッドを呼び出します。 - 結果が成功しなかった場合や例外が発生した場合は、onError メソッドを呼び出して、更新が失敗したことを呼び出し元に伝えます。
このコールバック ベースの実装は、メインスレッドをブロックしないため、メインセーフティです。ただし、作業が完了したときに呼び出し元に通知するには、コールバックを使用する必要があります。また、切り替えた BACKGROUND
スレッドでコールバックも呼び出します。
コルーチンからブロッキング呼び出しを呼び出す
ネットワークやデータベースにコルーチンを導入しなくても、コルーチンを使用してこのコードをメインセーフにすることができます。これにより、コールバックを削除し、結果を最初に呼び出したスレッドに渡すことができます。
このパターンは、大きなリストの並べ替えやフィルタリング、ディスクからの読み取りなど、コルーチン内部からブロッキングや CPU 負荷の高い処理を行う必要がある場合にいつでも使用できます。
ディスパッチャを切り替えるために、コルーチンは withContext
を使用します。withContext
を呼び出すと、ラムダのためだけに他のディスパッチャに戻ります。その後、そのラムダの結果で呼び出したディスパッチャに戻ります。
デフォルトでは、Kotlin コルーチンには Main
、IO
、Default
の 3 つのディスパッチャが用意されています。IO ディスパッチャはネットワークやディスクからの読み取りなどの IO 作業向けに、またデフォルトのディスパッチャは CPU 使用率の高いタスク向けに最適化されています。
TitleRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
この実装では、ネットワークとデータベースにブロッキング呼び出しを使用しますが、コールバック バージョンよりも少しシンプルです。
このコードでは、依然としてブロッキング呼び出しを使用しています。execute()
と insertTitle(...)
のどちらを呼び出しても、このコルーチンが実行されているスレッドがブロックされます。ただし、withContext
を使用して Dispatchers.IO
に切り替えることで、IO ディスパッチャのスレッドの 1 つがブロックされます。これを呼び出したコルーチン(Dispatchers.Main
で実行されている可能性あり)は、withContext
ラムダが完了するまで一時停止されます。
コールバック バージョンと比較すると、次の 2 つの重要な違いがあります。
withContext
は、呼び出したディスパッチャ(この場合はDispatchers.Main
)に結果を返します。コールバック バージョンは、BACKGROUND
エグゼキュータ サービスのスレッドでコールバックを呼び出しました。- 呼び出し元はこの関数にコールバックを渡す必要はありません。一時停止と再開を利用して、結果またはエラーを取得できます。
アプリを再度実行する
アプリを再度実行すると、新しいコルーチン ベースの実装がネットワークから結果を読み込んでいることがわかります。
次のステップでは、コルーチンを Room と Retrofit に統合します。
コルーチンの統合を続けるため、Room と Retrofit の安定版で suspend 関数のサポートを使用し、suspend 関数を使用して、先ほど作成したコードを大幅に簡素化します。
Room でのコルーチン
まず、MainDatabase.kt
を開き、insertTitle
を suspend 関数にします。
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
この場合、Room はクエリをメインセーフにして、バックグラウンド スレッドで自動的に実行します。ただし、このクエリはコルーチン内からのみ呼び出すことができます。
これで、Room でコルーチンを使用できるようになります。かなり便利です。
Retrofit のコルーチン
次に、コルーチンを Retrofit と統合する方法を見てみましょう。MainNetwork.kt
を開き、fetchNextTitle
を suspend 関数に変更します。
MainNetwork.kt
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
Retrofit で suspend 関数を使用するには、次の 2 つを行う必要があります。
- 関数に suspend 修飾子を追加する
- 戻り値の型から
Call
ラッパーを削除します。ここではString
を返していますが、複雑な JSON ベースの型を返すこともできます。それでも Retrofit の完全なResult
へのアクセスを提供したい場合は、一時停止関数からString
ではなくResult<String>
を返すことができます。
Retrofit は suspend 関数を自動的にメインセーフにするため、Dispatchers.Main
から直接呼び出すことができます。
Room と Retrofit を使用する
Room と Retrofit が suspend 関数をサポートするようになったため、リポジトリから suspend 関数を使用できます。TitleRepository.kt
を開いて、ブロッキング バージョンと比較しても、一時停止関数を使用するとロジックが大幅に簡素化されることを確認します。
タイトルRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
わあ、かなり短くなりましたね。どうなりましたか?一時停止と再開に依存することで、コードを大幅に短縮できます。Retrofit では、ここでは Call
ではなく、String
などの戻り値の型や User
オブジェクトを使用できます。これは安全です。suspend 関数内で、Retrofit
はバックグラウンド スレッドでネットワーク リクエストを実行し、呼び出しが完了するとコルーチンを再開できるためです。
さらに、withContext
も削除しました。Room と Retrofit はどちらもメインセーフな中断関数を提供しているため、Dispatchers.Main
からこの非同期処理を安全にオーケストレートできます。
コンパイラ エラーの修正
コルーチンに移行するには、関数のシグネチャを変更する必要があります。これは、通常の関数から suspend 関数を呼び出すことができないためです。このステップで suspend
修飾子を追加すると、実際のプロジェクトで関数を一時停止に変更した場合に何が起こるかを示すコンパイラ エラーがいくつか生成されました。
プロジェクトを確認し、関数を suspend に変更してコンパイラ エラーを修正します。それぞれの簡単な解決策は次のとおりです。
TestingFakes.kt
新しい一時停止修飾子をサポートするようにテスト フェイクを更新。
TitleDaoFake
- alt+Enter を押して、階層内のすべての関数に suspend 修飾子を追加します
MainNetworkFake
- alt+Enter を押して、階層内のすべての関数に suspend 修飾子を追加します
fetchNextTitle
をこの関数に置き換えます
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- alt+Enter を押して、階層内のすべての関数に suspend 修飾子を追加します
fetchNextTitle
をこの関数に置き換えます
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
refreshTitleWithCallbacks
関数は使用されなくなったため、削除します。
アプリを実行する
アプリを再度実行すると、コンパイル後に、ViewModel から Room、Retrofit まで、コルーチンを使用してデータが読み込まれていることがわかります。
お疲れさまでした。これで、このアプリをコルーチンを使用するように完全に切り替えることができました。最後に、先ほど行ったテストの方法について説明します。
この演習では、suspend
関数を直接呼び出すテストを作成します。
refreshTitle
はパブリック API として公開されているため、直接テストされ、テストからコルーチン関数を呼び出す方法が示されます。
前回の演習で実装した refreshTitle
関数は次のとおりです。
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
suspend 関数を呼び出すテストを作成する
2 つの TODO がある test
フォルダの TitleRepositoryTest.kt
を開きます。
最初のテスト whenRefreshTitleSuccess_insertsRows
から refreshTitle
を呼び出してみます。
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
refreshTitle
は suspend
関数であるため、Kotlin はコルーチンまたは別の suspend 関数からしか呼び出す方法を知りません。そのため、「Suspend 関数 refreshTitle はコルーチンまたは別の suspend 関数からのみ呼び出す必要があります」のようなコンパイラ エラーが発生します。
テストランナーはコルーチンについて何も知らないため、このテストを suspend 関数にすることはできません。ViewModel
のように CoroutineScope
を使用してコルーチンを launch
できますが、テストでは、戻る前にコルーチンを完了まで実行する必要があります。テスト関数が戻ると、テストは終了します。launch
で開始されたコルーチンは非同期コードであり、将来のある時点で完了する可能性があります。したがって、非同期コードをテストするには、コルーチンが完了するまでテストを待機させる方法が必要です。launch
は非ブロッキング呼び出しであるため、すぐに戻り、関数が戻った後もコルーチンを実行し続けることができます。テストでは使用できません。次に例を示します。
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
このテストは失敗することがあります。launch
の呼び出しはすぐに戻り、テストケースの残りの部分と同時に実行されます。テストでは refreshTitle
が実行されたかどうかを判断できないため、データベースが更新されたことを確認するなどのアサーションは不安定になります。また、refreshTitle
が例外をスローした場合、テストの呼び出しスタックではスローされません。代わりに、GlobalScope
の捕捉されなかった例外ハンドラにスローされます。
ライブラリ kotlinx-coroutines-test
には、suspend 関数を呼び出している間にブロックを行う runBlockingTest
関数があります。runBlockingTest
が suspend 関数を呼び出したり、launches
が新しいコルーチンを開始したりすると、デフォルトではすぐに実行されます。これは、suspend 関数とコルーチンを通常の関数呼び出しに変換する方法と考えることができます。
また、runBlockingTest
はキャッチされなかった例外を再スローします。これにより、コルーチンが例外をスローするタイミングを簡単にテストできます。
1 つのコルーチンでテストを実装する
refreshTitle
の呼び出しを runBlockingTest
でラップし、subject.refreshTitle() から GlobalScope.launch
ラッパーを削除します。
TitleRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
このテストでは、提供されたフェイクを使用して、refreshTitle
によって「OK」がデータベースに挿入されることを確認します。
テストが runBlockingTest
を呼び出すと、runBlockingTest
によって開始されたコルーチンが完了するまでブロックされます。次に、内部で refreshTitle
を呼び出すと、通常の一時停止と再開のメカニズムを使用して、データベース行がフェイクに追加されるのを待ちます。
テスト コルーチンが完了すると、runBlockingTest
が返されます。
タイムアウト テストを作成する
ネットワーク リクエストに短いタイムアウトを追加します。まずテストを作成してから、タイムアウトを実装しましょう。新しいテストを作成します。
TitleRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
このテストでは、提供された偽の MainNetworkCompletableFake
を使用します。これは、テストが継続されるまで呼び出し元を一時停止するように設計されたネットワークの偽物です。refreshTitle
がネットワーク リクエストを作成しようとすると、タイムアウトをテストするために永久にハングします。
次に、別のコルーチンを起動して refreshTitle
を呼び出します。これはタイムアウト テストの重要な部分です。タイムアウトは、runBlockingTest
が作成したコルーチンとは別のコルーチンで発生する必要があります。これにより、次の行の advanceTimeBy(5_000)
を呼び出すことができます。これにより、時間が 5 秒進み、他のコルーチンがタイムアウトします。
これは完全なタイムアウト テストであり、タイムアウトを実装すると合格します。
実行して、何が起こるか見てみましょう。
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
runBlockingTest
の機能の 1 つは、テスト完了後にコルーチンをリークさせないことです。テストの終了時に、起動コルーチンのような未完了のコルーチンがあると、テストは失敗します。
タイムアウトを追加する
TitleRepository
を開き、ネットワーク フェッチに 5 秒のタイムアウトを追加します。これを行うには、withTimeout
関数を使用します。
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
テストを実行します。テストを実行すると、すべてのテストに合格するはずです。
次の演習では、コルーチンを使用して高階関数を作成する方法を学習します。
この演習では、MainViewModel
の refreshTitle
をリファクタリングして、一般的なデータ読み込み関数を使用します。このコースでは、コルーチンを使用する高階関数を構築する方法を学びます。
現在の refreshTitle
の実装は機能しますが、スピナーを常に表示する一般的なデータ読み込みコルーチンを作成できます。これは、複数のイベントに応じてデータを読み込むコードベースで、読み込みスピナーが常に表示されるようにする場合に役立ちます。
現在の実装を確認します。repository.refreshTitle()
以外のすべての行は、スピナーを表示してエラーを表示するためのボイラープレートです。
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
高階関数でコルーチンを使用する
次のコードを MainViewModel.kt に追加します。
MainViewModel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
次に、この高階関数を使用するように refreshTitle()
をリファクタリングします。
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
読み込みスピナーの表示とエラーの表示に関するロジックを抽象化することで、データを読み込むために必要な実際のコードを簡素化しました。スピナーの表示やエラーの表示は、あらゆるデータ読み込みに簡単に一般化できますが、実際のデータソースと宛先は毎回指定する必要があります。
この抽象化を構築するために、launchDataLoad
は一時停止ラムダである引数 block
を受け取ります。suspend ラムダを使用すると、suspend 関数を呼び出すことができます。これが、この Codelab で使用してきたコルーチン ビルダー launch
と runBlocking
を Kotlin が実装する方法です。
// suspend lambda
block: suspend () -> Unit
一時停止ラムダを作成するには、suspend
キーワードで始めます。関数矢印と戻り値の型 Unit
で宣言が完了します。
独自の suspend ラムダを宣言する必要はあまりありませんが、このような繰り返しロジックをカプセル化する抽象化を作成するのに役立ちます。
この演習では、WorkManager からコルーチン ベースのコードを使用する方法を学習します。
WorkManager とは
Android には、遅延可能なバックグラウンド処理を行うためのさまざまな方法が用意されています。この演習では、WorkManager をコルーチンと統合する方法について説明します。WorkManager は、遅延可能なバックグラウンド処理用のライブラリであり、互換性、柔軟性、シンプルさを兼ね備えています。Android では、このようなユースケースに WorkManager を使用することをおすすめします。
WorkManager は Android Jetpack の一部であり、待機的実行と確実な実行というニーズの組み合わせをもつバックグラウンド処理のためのアーキテクチャ コンポーネントです。待機的実行とは、WorkManager がバックグラウンド処理を可能になり次第実行することを指します。確実な実行とは、たとえばアプリを終了した場合など、さまざまな状況下で WorkManager がその処理の開始ロジックを保持して実行することを指します。
そのため、WorkManager は最終的に完了する必要があるタスクに適しています。
WorkManager の使用が適したタスクの例を以下に示します。
- ログのアップロード
- 画像へのフィルタ適用と画像の保存
- ローカルデータとネットワークとの定期的な同期
WorkManager でコルーチンを使用する
WorkManager は、さまざまなユースケースに対応するため、ベースの ListanableWorker
クラスのさまざまな実装を提供します。
最もシンプルな Worker クラスを使用すると、WorkManager で同期オペレーションを実行できます。ただし、これまでの作業でコードベースをコルーチンと suspend 関数を使用するように変換してきたため、WorkManager を使用する最善の方法は、doWork()
関数を suspend 関数として定義できる CoroutineWorker
クラスを使用することです。
まず、RefreshMainDataWork
を開きます。すでに CoroutineWorker
を拡張しているため、doWork
を実装する必要があります。
suspend
doWork
関数内で、リポジトリから refreshTitle()
を呼び出し、適切な結果を返します。
TODO を完了すると、コードは次のようになります。
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
なお、CoroutineWorker.doWork()
は suspend 関数です。よりシンプルな Worker
クラスとは異なり、このコードは WorkManager の設定で指定された Executor では実行されず、代わりに coroutineContext
メンバー(デフォルトでは Dispatchers.Default
)のディスパッチャーを使用します。
CoroutineWorker のテスト
テストなしでコードベースを完成させることはできません。
WorkManager には、Worker
クラスをテストするためのさまざまな方法が用意されています。元のテスト インフラストラクチャについて詳しくは、ドキュメントをご覧ください。
WorkManager v2.1 では、ListenableWorker
クラス、ひいては CoroutineWorker をより簡単にテストするための新しい API セットが導入されています。コードでは、これらの新しい API の 1 つである TestListenableWorkerBuilder
を使用します。
新しいテストを追加するには、androidTest
フォルダの RefreshMainDataWorkTest
ファイルを更新します。
ファイルの内容は次のとおりです。
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
テストの前に、WorkManager
にファクトリを伝えて、偽のネットワークを挿入できるようにします。
テスト自体は TestListenableWorkerBuilder
を使用してワーカーを作成し、startWork()
メソッドを呼び出して実行します。
WorkManager は、コルーチンを使用して API 設計を簡素化できる例の 1 つにすぎません。
この Codelab では、アプリでコルーチンを使用するために必要な基本事項について説明しました。
取り上げた内容は次のとおりです。
- UI と WorkManager ジョブの両方からコルーチンを Android アプリに統合して非同期プログラミングを簡素化する方法
ViewModel
内でコルーチンを使用して、メインスレッドをブロックせずにネットワークからデータを取得してデータベースに保存する方法。- また、
ViewModel
が終了したときにすべてのコルーチンをキャンセルする方法も説明します。
コルーチン ベースのコードをテストする場合、動作のテストと、テストからの suspend
関数の直接呼び出しの両方について説明しました。
詳細
Android での高度なコルーチンの使用方法について詳しくは、「Kotlin Flow と LiveData を使用した高度なコルーチン」のコードラボをご覧ください。
Kotlin コルーチンには、この Codelab で取り上げていない機能が多数あります。Kotlin コルーチンについて詳しくは、JetBrains が公開しているコルーチン ガイドをご覧ください。Android でのコルーチンの使用パターンについて詳しくは、「Kotlin コルーチンでアプリのパフォーマンスを改善する」もご覧ください。