前回Roomをお勉強したのの続きです。
Kotlin勉強するなら非同期はコルーチンでやりたいよね。Rxは勉強することが多すぎるし。
ってことで、最初はまずCodeLabsのUsing Kotlin Coroutines in your Android Appでお勉強を始めます。
日本語は大いに意訳、要約です(でも面倒になるとGoogleさんに力を借りた直訳風になりますwあと、口語になったり文語になったり)。必ず原文と照らし合わせながら参照してください。
日本語訳以外に、個人的に嵌まったところ(CodeLab内で触れていなくて罠になっているところ)も覚え書きしていきます。
斜体部分が、個人的な感想、メモ、意見、独り言です。
対象者
- Java,Kotlinを読める
- Android Architecture Components(以下AAC)のViewModel 、LiveDataについて何となく知っている
- 非同期処理についてだいたい理解している(MainThreadとWorkerThreadの違いが分かる)
CodeLab説明
1. Introduction(紹介)
このCodeLabを勉強すると、Kotlin Coroutinesの使い方が分かるようになります。Androidに於けるバックグラウンド非同期処理の新しい書き方で、コールバックの嵐になっていた非同期処理をシンプルに、直列的に書けるようになります。
AACを用いて作成された、コールバックタイプの非同期処理で書かれたサンプルプロジェクトを元にして学習します。
この学習を終えれば、非同期API処理をコルーチンで書き換えられるくらいには経験を積めるでしょう。コルーチンの主要な使い方を学び、テストをどう書くかについても学べるでしょう。
What you'll learn(何が学べるの)
- コルーチンで書かれたコードを呼び出して結果を得る方法
-
suspend functon
を使用して非同期コードを直列的に書く方法 -
launch
とrunBlocking
を使用してコードが実行される方法を制御する方法 -
suspendCoroutine
を使用して既存のAPIをコルーチンに変換する方法 - AACででコルーチンを使用する方法
- コルーチンをテストするためのベストプラクティス
Prerequisites(前提条件)
- AACの
ViewModel
,LiveData
,Repository
, andRoom
について知っている - 拡張関数や、ラムダについても含む、Kotlin文法を知っている
- 旧来のAndroidに於けるスレッドの概念を理解している(メインスレッドとワーカースレッドについて)。また、コールバックの書き方を知っている
What you'll need(必要環境)
- Android Studio 3.3以上
2. Getting set up(準備)
Zipをダウンロードするか、下記プロジェクトをGitからクローンする。
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
3. Run the starting sample app(最初のサンプルアプリを実行しよう)
- zipをダウンロードした場合は、まず解凍
- Android Studioで
kotlin-coroutines-start
プロジェクトを開く
Gradle Plugin等のバージョンを上げるか聞かれた場合、 上げない 方が良いです。本筋で無いところで修正が必要になることがあります。 - Runボタンをクリックして実行。(Lollipop以上の端末かエミュレーターが必要。つまりminSDK=21)
このstartアプリは、ユーザーが画面のどこかをタップした1秒後にSnackbarを表示するのに、スレッドを使っています。やってみると、*"Hello, from threads!"*と僅かな遅延後に表示されます。まず始めに、この処理をコルーチンを使用した書き方に変換します。
このアプリはAACを用いて、MainActivity
のUIコードと、MainViewModel
のアプリケーションロジックとを分離しています。このような構成のプロジェクトに馴染めるよう、少し時間を取ってください。
-
MainActivity
は画面表示を担当。クリックリスナーを登録したり、スナックバーを表示したり。イベントはMainViewModel
に渡し、表示の更新は、MainViewModel
の持つLiveData
を基に行う -
MainViewModel
はonMainViewClicked
でイベントをハンドリングし、MainActivity
とのやりとりはLiveData
を通して行う -
Executors
はBACKGROUND
を定義。これによりバックグラウンドスレッドで実行が出来る
※BACKGROUND
は、ExecutorService
クラスののグローバル変数ですね。
あまり一般的に見るスレッド処理ではない気がするけど・・・それとも、これが標準なのかな?私は簡単な物はThread(Runnable)
で済ましちゃいますが・・・ -
MainViewModelTest
でViewModelのテストを実装しています。
Adding coroutines to a project(コルーチンをプロジェクトに追加する)
Kotlinでコルーチンを使用するには、coroutines-core
ライブラリをbuild.gradleに含める必要があります。このプロジェクトではもうやってあげちゃってるけどね。
Androidにおけるコルーチンは、コアライブラリ、Android拡張として用意されています。
- kotlinx-corountines-core - Kotlinでコルーチンを使用するメインインターフェース
- kotlinx-coroutines-android - コルーチンでのAndroidメインスレッドをサポート
このプロジェクトはもう入れてあげてあるから必要ないけど、自分のプロジェクトに入れるときには、appモジュール
のbuild.gradle
にdependenciesを追記してください。
2019/05/18時点での最新バージョンは下記の通りです
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0'
[補足の内容]
RxJavaを既に自分のプロジェクトで使っている人は、RxJavaでコルーチンを使えるようにしたライブラリ kotlin-coroutines-rxが使えます。
4. Coroutines in Kotlin(Kotlinでコルーチン)
Androidにおいては、メインスレッド*(=UIスレッド)*を以下にブロッキングしないかと言うことはとても大事。メインスレッドはシングルスレッドですべてのUIの更新を制御しています。クリックやその他のUIコールバックも呼び出します。良好なユーザーエクスペリエンスを提供するにはこのスレッドがスムースに実行されていなければなりません。
メインスレッドは16msかそれ以上の頻度で、UIを更新できなければなりません。これは60FPSに相当します。これより時間のかかる処理は普通にあります。例えばでかいJSONファイルをparseするとか、デカいデータセットをデータベースに書き込むとか、ネットワークからデータを取得するとか。そんなコードをそのままメインスレッドで呼んでしまうと、アプリの反応が悪くなったり、カクついたり、最悪はフリーズします。メインスレッドの処理を長くブロックしてしまうと、アプリケーションはクラッシュして**ANR(Application Not Responding dialog)**ダイアログを表示します。
「アプリケーションXXは反応していません〜」というダイアログですね
コルーチンがこれをどうやって解決するかの紹介は、動画を観てね。
動画の内容は頑張って理解して下さい
The callback pattern(コールバックパターン)
重い処理を別スレッドで動かすパターンの一つの実際として、コールバック手法があります。バックグラウンドスレッドでコールバックを使うと、処理が終了したときに結果をメインスレッドで受け取ることが出来ます。
コールバックの一つのパターンを見てみましょう。
// コールバックと重い処理
@UiThread
fun makeNetworkRequest() {
// 遅いネットワークリクエストは別スレッドで動く
slowFetch { result ->
// 結果が返ったら, コールバックがそれを受け取る
show(result)
}
// slowFetchメソッドを呼び出したら、結果を待たずに抜けます
}
AsyncTaskと
かAsyncTaskLoader
を使ったサンプルの方が、Androiderには馴染みが深い気がするけど、その記述はややこしいからこのサンプルなのかな・・・
@UiThread
アノテーションが付いているので、このメソッドはメインスレッド上で素早く終了する必要があります。しかしslowFetch
は遅いので、メインスレッドはその処理を待っていることは出来ません。show(result)
をコールバックにすることで、slowFetch
をバックグラウンドスレッドで実行し、結果を終了時に取得することを可能にしています。
Using coroutines to remove callbacks(コルーチンを使ってコールバックを削除する)
コールバック手法は悪くないんだけど、コードが読みづらくなるんだよね。それに、例外処理などの一部はコールバックで受け取れません。
Kotlinコルーチンでは、コールバックスタイルのコードを、直列的に変換できます。直列的なコードは基本的に読みやすく、例外なども使えるようになります。
最終的には両者は同じことをしています。長い処理の結果を待ち、処理を続けることが出来ます。でもコードの見た目は全然異なります。
suspend
キーワードは、関数をコルーチン向けにマークするKotlin文法です。コルーチンがsuspend
キーワードの付いた関数を呼んだとき、関数が戻るまでメインスレッドをブロックする代わりに、結果が戻るまで中断し、結果が出たら中断したところから処理を再開します。結果を待っている間は、他の関数やコルーチンの処理が出来るようにそちらのブロックを解除します。
下の例では、makeNetworkRequest()
とslowFetch()
は共にsuspend関数です。
// 重い処理をコルーチンで
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch もsuspend関数で、メインスレッドをブロックする代わりに
// makeNetworkRequest を 結果が戻るまで`suspend` する
val result = slowFetch()
// 結果が届いたら次の処理を続ける
show(result)
}
// コルーチンを使ったmain-safeなメソッド
suspend fun slowFetch(): SlowResult { ... }
main-safeってメインスレッドをブロックしない、くらいの意味かな?
コールバックバージョンと同じように、makeNetworkRequest
は@UiThread
が付いているためメインスレッドに即座に戻る必要があります。つまり、通常はslowFetchのようなブロッキングメソッドを呼ぶことは出来ません。ここがsuspendキーワードの魔法のようなところです。
[補足内容]
重要: suspendキーワードは、実行されるスレッドを特定しません。バックグラウンドスレッドかも知れないし、メインスレッドかも知れません
コールバックの時のコードと比べると、コルーチンでのコードは、直列的に書けるため、より少ないコードで同じ結果が得られます。なので複数の非同期処理があっても、コールバック地獄にならずに済み、完結に書くことが出来ます。例えば、ネットワークの複数のエンドポイントからデータを取得してデータベースに保存するような処理も、コールバックを使わないで一連の処理のように書くことが出来ます。
// コルーチンを使ってネットワークからデータを取得してデータベースに保存する
// @WorkerThread が付いているので、この関数はメインスレッドから呼べない。エラーになる
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch と anotherFetch は suspend 関数
val slow = slowFetch()
val another = anotherFetch()
// save は普通の関数で大抵スレッドをブロックする
database.save(slow, another)
}
// コルーチンを使ったmain-safeなメソッド
suspend fun slowFetch(): SlowResult { ... }
// コルーチンを使ったmain-safeなメソッド
suspend fun anotherFetch(): AnotherResult { ... }
次のセクションでstartサンプルをコルーチンに書き換えていきます。
4. Controlling the UI with coroutines(コルーチンでUIを制御する)
このエクササイズでコルーチンを使って数秒遅れにメッセージを出します。AndroidStudioでkotlin-coroutines-start
を開いて下さい。
Add a coroutine scope to MainViewModel(MainViewModelにコルーチンscopeを追加する)
Kotlinでは、コルーチンはすべてCoroutineScope内で実行されます。スコープはジョブを通してコルーチンの寿命を制御します。スコープのジョブをキャンセルしたとき、スコープ内で開始されたすべてのコルーチンがキャンセルされます。Androidで、ユーザーが画面を離れたときに処理をすべてキャンセルするのに、スコープが便利です。スコープにより、デフォルトのディスパッチャーを使うことが出来ます。ディスパッチャーは、コルーチンを実行するスレッドを制御します。
MainViewModel
でコルーチンを使うために、まずスコープを次のように作成します。
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
この例では、uiScope
は、AndroidでのメインスレッドであるDispatchers.Main
でコルーチンを開始します。メインスレッドで開始されたコルーチンはsuspendされている間スレッドをブロックしません。ViewModel
のコルーチンはだいたいいつもメインスレッド上のUIを更新するのに使われるので、メインスレッドで実行されることがデフォルトなのは妥当です。このCodeLabの後半でやりますが、コルーチンは開始された後でもいつでもディスパッチャーを変更することが出来ます。例えば、メインディスパッチャーで処理を開始した後、Jsonなんかの重いパースをするときには別のディスパッチャーを使うなど出来ます。
[補足内容]
CoroutineContextについて
CoroutineScopeはCoroutineContext
をパラメーターに取ることが出来ます。CoroutineContext
は、コルーチンを構成する一連の属性です。 スレッド化ポリシー、例外ハンドラなどを定義できます。
上の例では、CoroutineContext plus演算子を使用して、スレッド化ポリシー(Dispatchers.Main)とジョブ(viewModelJob)を定義しています。 結果のCoroutineContextは、両方のコンテキストの組み合わせです。
直訳で済みません。Googleさんありがとう
Cancel the scope when ViewModel is cleared(ViewModelがクリアされたらスコープをキャンセルする)
ViewModelクラスが破棄されるとき、onCleared
が呼ばれます。だいていユーザーが画面を離れるときはスコープの処理をキャンセルしたいものでしょう。次のようにします。
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
viewModelJob
はuiScope
で起動されていたので、viewModelJob
がキャンセルされたときは同時にuiScope
で起動されたすべてのコルーチンがキャンセルされます。不要になったコルーチンがキャンセルできることは、バックグラウンドでの不必要な処理やメモリリークを防ぐために重要です。
重要
スコープ内で開始されたすべてのコルーチンをキャンセルするには、CoroutineScopeにJobを渡す必要があります。 そうでない場合、スコープはアプリが終了するまで実行されます。 それが意図したものではない場合、メモリがリークしていることでしょう。
CoroutineScopeコンストラクタで作成されたスコープは暗黙のジョブを追加します。これはuiScope.coroutineContext.cancel()を使用して取り消すことができます。
Use viewModelScope to avoid boilerplate code(定型コードの繰り返しを避ける為にviewModelScopeを使う)
これまでのコードを、全部のViewModelクラスに書くことになるけど、それは当然大量のボイラープレート(繰り返し定型コード)を産んでしまいます。そこで、ライブラリlifecycle-viewmodel-ktx
の出番です。このライブラリを使うには、appモジュールの`build.gradleに以下のように追記します。でも実はもうこのステップもプロジェクトに入ってるけどね。
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-alpha02"
2019/05/18現在の最新版は2.2.0-alpha01みたいですが、それに上げるとビルドが通らなくなります。
このライブラリは、viewModelScope
をViewModel
クラスの拡張関数として追加します。このスコープはDispatchers.Main
にバインドされていて、ViewModelがクリアされると自動的にキャンセルされます。
すべてのViewModelクラスに新しいスコープを作るコードを書くことなく、viewModelScope
を使うことが出来ます。ライブラリが初期化や破棄のすべての面倒を見てくれます。
次のコードは、viewModelScope
を使ってバックグラウンドスレッドでネットワークリクエストを行うコルーチンを起動する方法です。
class MainViewModel : ViewModel() {
// UIスレッドをブロックすること無くネットワークリクエストを送る
private fun makeNetworkRequest() {
// viewModelScope でコルーチンを起動する
viewModelScope.launch(Dispatchers.IO) {
// slowFetch()
}
}
// onCleared()のオーバーライドは不要
}
なんかただのサンプルコードと、プロジェクトを書き換えるコードなのかが分かりづらいですね
Switch from threads to coroutines(スレッドをコルーチンに置き換える)
MainViewModel.kt
の"TODO"コメントを見つけてください。
/**
* Wait one second then display a snackbar.
*/
fun onMainViewClicked() {
// TODO: Replace with coroutine implementation
BACKGROUND.submit {
Thread.sleep(1_000)
// use postValue since we're in a background thread
_snackBar.postValue("Hello, from threads!")
}
}
このコードはバックグラウンドスレッドで実行するためにBACKGROUND *(ExecuterServiceのインスタンス)*を使用しています。sleep
は現在のスレッドをブロックするので、もしこれがメインスレッドから呼ばれていたら、UIの処理はその間固まってしまいます。画面をタップして1秒後、snackbar表示をリクエストしています。
コルーチンのコードには次のように書き換えます。launch
とdelay
をimportしてね。
/**
* Wait one second then display a snackbar.
*/
fun onMainViewClicked() {
// viewModelScopeでコルーチンを起動
viewModelScope.launch {
// 1秒間このコルーチンを中断(suspend)
delay(1_000)
// main dispatcherを再開
// _snackbar.value はメインスレッド上からなら直接値を書き換えられる
_snackBar.value = "Hello, from coroutines!"
}
}
このコードは1秒待ち、snackbarを表示するという、同じ事をしています。しかし重要な違いがあります。
-
viewModelScope.launch
はviewModelScope
でコルーチンを開始します。viewModelScope
に渡したジョブがキャンセルされるとき、このスコープ/ジョブはすべてキャンセルされることを意味します。ユーザーがActivityをdelay
が戻る前に離れた場合、ViewModelのデストラクタにおいてonCleared
が呼ばれた際にこのコルーチンは自動的にキャンセルされます。 -
viewModelScope
はデフォルトのディスパッチャーとしてDispatchers.Main
を保持していることから、このコルーチンはメインスレッドで呼ばれます。他のスレッドを使う方法は後ほど見ていきます。 -
delay
関数はsuspend
関数です。Android Studioの左側にアイコン[Suspend function call]が表示されます。このコルーチンはメインスレッドで実行されるものの、delay
関数はスレッドを1秒間ブロックするわけではありません。代わりに、ディスパッチャーが1秒後にこのコルーチンを再開することをスケジューリングします。
実行してみましょう。画面をタップした1秒後にスナックバーが表示されるはずです。
なぜかこのタイミングで、私の環境ではMETA-INF/xxxx
のビルドエラーが起きるようになりました。対処法については、前回の記事を参照下さい。
次のセクションでは、この関数をテストする方法について考えます。
6. Testing coroutines through behavior(コルーチンのテスト)
ここまで書いたコルーチンのコードのテストの書き方を学びます。スレッドを使っていたときと同じようなスタイルで書けることが分かるでしょう。このCodeLabの後半では、コルーチンと直接対話するテストの書き方も示します。
[補足の内容]
kotlinx-coroutines-testというライブラリが、experimentalで開発進行中なようです。
CodeLabでは、ライブラリがexperimentalであるため、stableなAPIを使った書き方を中心にしています。
セクションの最後にはkotlinx-coroutines-testライブラリでの書き方も載せておきます。
kotlinx-coroutines-testライブラリがstableになったらこの辺の記述が変わりそうですね。
Review the existing test(既存のテストのレビュー)
androidTest
フォルダのMainViewModelTest.kt
を開いて下さい。
コードは割愛します。
各テストの実行前に、2つのことが起こります。
- JUnitでのテストの前後に実行するコードについてのルールに、
InstantTaskExecutorRule
が使われます。InstantTaskExecutorRule
はテストの実行中にLiveDataがメインスレッドにすぐにポストされる設定のルールです。 -
setup()
メソッドの中で、subject
フィールドは新しいMainViewModel
に初期化されます。
初期化の後、一つのテストが定義されています。
コードは割愛します。
このテストは、onMainViewClicked
を呼び、snackbar (MainViewModelのLiveDataなやつのことです。実際にActivityに表示されるSnackbarのことではありません) の値が変わるのを待ちます。その時ヘルパークラスのassertSendsValues
を使います。これはLiveData
にデータが送られるまで2秒間待つようになってます。このメソッドの中を読む必要はこのCodeLabではありません。
このテストはViewModel
のpublic APIにのみ依存しています。onMainViewClicked
がクリックされたとき、snackbarには*"Hello, from threads!"*が送られるはずです。
我々はpublic APIを変更してはいません。このメソッド呼び出しは相変わらずsnackbarを更新します。つまり、コルーチンに書き換えることは、既存のテストコードを破壊はしません。
とはいえ既存のテストの書き方によるとは思うけど
Run the existing test(既存のテストを実行する)
1.〜3. はテストの実行の仕方なので割愛します。
テストは、これまでのコードを実装していると、次のような結果で失敗します。
expected: Hello, from threads!
but was : Hello, from coroutines!
Update failing test to pass(失敗したテストを成功するように更新する)
メソッドを挙動を書き換えているので、テストは失敗したのです。"Hello, from threads!" ではなく、"Hello, from coroutines!"にしました。
assertionを書き換えて、メソッドの新しい挙動に合わせます。
文字列の部分を置き換えるだけなのでコードは割愛します。
テストを再実行すると、テストはパス *(=成功)*するはずです。
public APIのみでテストすることで、テストの構造を変更することなく、テストをバックグラウンドスレッドからコルーチンに変更することができました。
次に、コールバックAPIを用いたコードをコルーチンに書き換える方法について学びます。
補足内容は省きます。いずれこっちが本筋になりそうですけど。Rxっぽく書けるようになるのかな?
7. Converting existing callback APIs with coroutines(既存のコールバックAPIをコルーチンに置き換える)
コールバックAPIで書かれたコードをコルーチンに置き換える方法について学びます。
Android Studioでkotlin-coroutines-repository
プロジェクトを開いて下さい。
startプロジェクトと同じく、Gradle Plugin等のアップデートはしない方が良いです
このアプリもAACを使い、ネットワークとデータベースを使うデータレイヤーを実装するよう前回までのプロジェクトを拡張しています。画面がタップされたら、新しいタイトルをネットワークから取得し、データベースに保存後、画面に表示します。新しいクラスをざっと眺めましょう。
-
MainDatabase
Roomを使ったデータベースで、Title
を保存、及び読み出す -
MainNetwork
新しいタイトルをネットワークAPIから取得する。タイトルの取得には、FakeNetworkLibrary.kt
で定義されている偽装ネットワークライブラリを使う。このライブラリはランダムにエラーを返す -
TitleRepository
ネットワークとデータベースからのデータを組み合わせて、タイトルを取得または更新するための単一のAPIを実装 -
MainViewModelTest
MainViewModel
のテスト -
FakeNetworkCallAwaitTest
テストコード。このCodeLabの後半で完成予定
Explore the existing callback API(既存のコールバックAPIについて)
MainNetwork.kt
を開きfetchNewWelcome()
の定義を見て下さい。
コードは割愛します。
TitleRepository.kt
を見ると、fetchNewWelcome()
がコールバックパターンを用いてネットワーク呼び出しをするのにどう使われているかが分かります。
この関数はFakeNetworkCall
を返します。それに対してコールバックを登録できます。fetchNewWelcome
の呼び出しは遅く長いネットワーク処理を別スレッドで行い、addOnResultListenerに結果オブジェクトを返します。addOnResultListener
で渡したコールバックが、要求が完了したとき、またはエラーが発生したときに呼び出されます。
直訳すると分かりづらいけど、コールバックスタイル=addOnXXXXXListenerみたいにイベントリスナーを登録していく方式、ということで、Androidの既存の実装ではお決まりの手法ですね。確かにこのコードは結構読みづらくなるんですよね。ラムダとかでだいぶ変わりはしますが、それでも連続処理したいときなんかは非常にネストが深くなって面倒です。それをコルーチンなら!ということです。
Convert the existing callback API to a suspend function(コールバックAPIをsuspend functionに書き換える)
refreshTitle
関数は、この時点では、FakeNetworkCall
のコールバックを使用しています。この練習でのゴールは、ネットワークAPIの呼び出しを、susnpend関数に変えることでrefreshTitle
をコルーチンで書けるようになることです。
コールバックで書かれたAPI呼び出しをsuspend関数に変換できるsuspendCoroutine
がKotlinには用意されています。
suspendCoroutine
を呼び出すと、直ちにカレントコルーチンが中断されます。suspendCoroutine
はそのコルーチンを再開するためのcontinuation
オブジェクトを返します。continuation
は文字通りのことをします。すなわち、中断されたコルーチンを継続、中断するのに必要なすべてのコンテキストを保持することです。
suspendCoroutine
が提供するcontinuation
には2つの関数があります。resume
とresumeWithException
です。どちらの関数も呼び出しが行われると直ぐにsuspendCoroutineが再開されます。
suspendCoroutine
は、コールバックを待つ前に使ってコルーチンを中断します。コールバックが呼ばれたらその戻り値を使って再開するのに、resume
かresumeWithException
を呼びます。
suspendCoroutine
のサンプルは次のようになります。
// suspendCoroutineサンプル
/**
* 文字列をコールバックするクラス
*/
class Call {
fun addCallback(callback: (String) -> Unit)
}
/**
* コールバックベースのAPIをsuspend関数として公開し、コルーチンで使用できるようにする
*/
suspend fun convertToSuspend(call: Call): String {
// 1: suspendCoroutineはすぐにコルーチンを *suspend* する
// そのブロックから渡されたcontinuationオブジェクトからのみ *resumed* される
return suspendCoroutine { continuation ->
// 2: suspendCoroutineをコールバック登録のために渡す
// 3: コールバックを渡して結果を待つ
call.addCallback { value ->
// 4: use continuation.resume to *resume* the coroutine
with the value. The value passed to resume will be
the result of suspendCoroutine.
// 4: continuation.resumeで コルーチンを *resume* する。
// 値valueはsuspendCoroutineの結果である
continuation.resume(value)
}
}
}
この例はsuspendCoroutine
がAPIベースのコールバックCall
をsuspend関数で使う方法を示しています。こうすると、Call
を直接次のように使うことが出来ます。
// コールバックAPIをコルーチンで使うためのconvertToSuspendの使い方のサンプル
suspend fun exampleUsage() {
val call = makeLongRunningCall()
convertToSuspend(call) // 長い処理が終わるまでsuspendする
}
このパターンを使ってFakeNetworkCallでsuspend関数を公開することができます。これによりコルーチンでコールバックベースのネットワークAPIを使うことができます。
この例ははっきり言って良く分からない・・・
What about cancellation?(キャンセルについて)
キャンセルを意識する必要が無いときは、suspendCoroutine
が良いでしょう。しかし、通常はキャンセルを考慮する必要があります。そういう場合はsuspendCancellableCoroutine
を使えばいいでしょう。
Use suspendCoroutine to convert a callback API to coroutines(suspendCorutineを使ってコールバックAPIをコルーチンに置き換える)
TitleRepository.kt
を下までスクロールすると、TODOコメントがあります。
そこを下記のようにFakeNetworkCall<T>
の拡張関数を定義します。
suspend fun <T> FakeNetworkCall<T>.await(): T {
return suspendCoroutine { continuation ->
addOnResultListener { result ->
when (result) {
is FakeNetworkSuccess<T> -> continuation.resume(result.data)
is FakeNetworkError -> continuation.resumeWithException(result.error)
}
}
}
}
kotlin.coroutines
のimportを、間違えてexperimentalの方にしないように!
この拡張関数は、suspendCoroutine
を用いて、コールバックベースのAPI処理をsuspend関数に変換します。コルーチンはawaitを呼び出して直ぐに中断しネットワーク呼び出しの結果が届くまで待つことが出来ます。結果はawait
の戻り値として返され、エラー時は例外が発生します。
このコードは次のように使うことが出来ます。
// awitの使い方サンプル
suspend fun exampleAwaitUsage() {
try {
val call = network.fetchNewWelcome()
// fetchNewWelcomemが結果を返すかエラーを投げる課するまでsuspendする
val result = call.await()
// resumeにより、awaitが結果を返す
} catch (error: FakeNetworkException) {
// resumeWithExceptionは、awaitに例外を投げる
}
}
await
関数の宣言を見みてみます。suspend
キーワードは、Kotlinに対しコルーチンで使えることを示しています。その結果、他のsuspendCoroutine
のようなsuspend関数を呼ぶことが出来ます。残りの、fun <T> FakeNetworkCall<T>.await()
部分のでは、FakeNetworkCall
クラスのオブジェクトすべてにawait
という名の拡張関数を定義していることを意味します。FakeNetworkCall
クラスを変更する物ではないですが、Kotlinから呼ばれたとき、これはpublic関数として扱われます。await
の戻り値はT
です。
What is an extension function?(拡張関数って?)
Kotlin初心者なら、拡張関数について初めて触れるかも知れません。拡張関数はクラスを書き換えるものではありませんが、this
を第一引数として受け取れる、新しい関数を導入することが出来る仕組みです。
fun <T> await(this: FakeNetworkCall<T>): T
await
関数に対して、this
にはFakeNetworkCall<T>
がバインドされて渡されます。だからそのメンバーメソッドであるaddOnResultListener
をawait
内で呼び出せるのです。
まとめると、このシグネチャ(宣言)は、本来コルーチン用に構築されたのではないクラスに、await()というsuspend関数を追加していることを意味します。 このアプローチは、実装を変更せずにコルーチンをサポートするためにコールバックベースのAPIを更新するために使用できます。
次のエクササイズでは、await()
のテストを書き、テストから直接コルーチンを呼ぶ方法について学びます。
8. Testing coroutines directly(コルーチンを直接テストする)
このエクササイズでは、suspend関数を直接呼び出すテストの書き方を学びます。
await
はpublicとして公開されているため、直接テスト出来るべきです。テストからコルーチン関数を呼び出す方法を示します。
前回作成したawait
関数を見て下さい。
suspend fun <T> FakeNetworkCall<T>.await(): T {
return suspendCoroutine { continuation ->
addOnResultListener { result ->
when (result) {
is FakeNetworkSuccess<T> -> continuation.resume(result.data)
is FakeNetworkError -> continuation.resumeWithException(result.error)
}
}
}
}
Write a test that calls a suspend function(suspend関数を呼ぶテスト)
androidTest
フォルダのFakeNetworkCallAwaitTest.kt
ファイルを開いて下さい。二つの TODOがあります。
二つ目のテスト、whenFakeNetworkCallFailure_throws
でawait
を呼んでみてください。
@Test(expected = FakeNetworkException::class)
fun whenFakeNetworkCallFailure_throws() {
val subject = makeFailureCall(FakeNetworkException("the error"))
subject.await() // Compiler error: Can't call outside of coroutine
}
await
はsuspend関数ですから、Kotlinはコルーチンや他のsuspend関数以外からそれを呼び出す手段がありません。というわけで、これではコンパイルエラーになります。エラーメッセージは*"Suspend function 'await' should be called only from a coroutine or another suspend function."*という感じでしょう。
test runnerはコルーチンについては何も知りません。なのでこのテストをsuspend関数にすることは出来ません。ViewModel
でやったように、CoroutineScope
を使ってコルーチンを起動することは出来ますが、テストは、テストメソッドが戻る前にコルーチンの処理が終了しなければなりません。テスト関数が戻れば、テストは終わってしまいます。launch
で起動されたコルーチンは非同期処理なので、未来に終了します。(テストメソッドが戻った時点で終わっていると限らない) 非同期処理のテストをするには、どうにかして、テストにコルーチンの処理の終了を待たせなくてはなりません。launch
はnon-blockingなので、呼ばれるとただちに戻り、関数が戻った後もコルーチンの処理を継続できるわけで、テストで使う事は出来ません。例えば、
// launchを使ったテストのサンプル(常に失敗)
@Test(expected = FakeNetworkException::class)
fun whenFakeNetworkCallFailure_throws() {
val subject = makeFailureCall(FakeNetworkException("the error"))
// launchはコルーチンを開始し、直後に戻る
GlobalScope.launch {
// これは非同期処理なので、テストメソッドが完了した後に呼ばれる可能性がある
subject.await()
}
// test関数は直ぐに終了するため、await()の起こした例外を検知できない
}
このテストは常に失敗します。launch
の呼び出しは直ぐに終わり、このテストケースも終了します。await
からの例外は、テストの終了前にも、後にも起こりえますが、例外オブジェクトはtestのコールスタックへは投げられません。コルーチンのスコープ内の未処理例外ハンドラに投げられてしまいます。
KotlinにはrunBlocking
という関数があります。これはsuspend関数が呼ばれている間、その関数の処理をブロックします。runBlocking
内でsupend関数を呼ぶと、通常は中断される処理を、普通の関数の実行のようにブロックすることが出来ます。suspend関数を通常関数の呼び出しのように変換できる手法と考えてよいでしょう。
runBlocking
はコルーチンをあたかも普通の関数のように呼ぶので、例外もまた普通の関数のように投げます。
Important(重要)
runBlocking
は呼び出したスレッドをブロックします。コルーチンは同じスレッド内で同期的に処理されます。だから普通のアプリの中では使っちゃダメだよ。launch
を使ってね。
runBlocking
はAPIのテストみたいな時にだけ使いましょ。
補足内
前述のkotlinx-coroutines-test ライブラリに、runBlockingTest
というのが追加されているようです。
await
呼び出しをrunBlocking
で囲みましょう。
@Test(expected = FakeNetworkException::class)
fun whenFakeNetworkCallFailure_throws() {
val subject = makeFailureCall(FakeNetworkException("the error"))
runBlocking {
subject.await()
}
}
一つ目のテストも同じようにrunBlocking
を使って実装します。
@Test
fun whenFakeNetworkCallSuccess_resumeWithResult() {
val subject = makeSuccessCall("the title")
runBlocking {
Truth.assertThat(subject.await()).isEqualTo("the title")
}
}
テストを実行してみましょう。2つとも通過するはずです!
次のエクササイズでは、Repository
とViewModel
からデータをfetch(取り出す)のにコルーチンを使う方法について学びます。
9. Using coroutines on a worker thread(ワーカースレッドでコルーチンを使う)
ここでは、スレッドでの処理をコルーチンに変更する方法について学びます。これによりTitleRepository
の実装を完成できます。
Review the existing callback code in refreshTitle(refreshTitleの既存のコールバックの実装を見る)
fun refreshTitle(onStateChanged: TitleStateListener) {
// 1: ネットワークリクエスト開始
onStateChanged(Loading)
val call = network.fetchNewWelcome()
// 2: ネットワークリクエストの完了やエラーを受け取るためにコールバックを登録
call.addOnResultListener { result ->
when (result) {
is FakeNetworkSuccess<String> -> {
// 3: 新しいtitleをバックグラウンドスレッドで保存
BACKGROUND.submit {
// insertTitleはバックグラウンドスレッドで実行
titleDao.insertTitle(Title(result.data))
}
// 4: 呼び出し元にリクエストの成功を通知
onStateChanged(Success)
}
is FakeNetworkError -> {
// 5: 呼び出し元にクエストの失敗を通知
onStateChanged(
Error(TitleRefreshError(result.error)))
}
}
}
}
TitleRepository.kt
のrefreshTitle
メソッドは、呼び出し元とのネットワークリクエストのローディングやエラー状態の通知をやりとりするのにコールバックを用いて実装されています。二つのコールバックがあり、コードの可読性を下げています。何をやっているかは次の通りです。
- リクエストを開始する前に、コールバックはリクエストが
Loading
状態になったことを通知する - ネットワーク処理の結果を待つため、
FakeNetworkCall
にコールバックを登録 - 新しいtitleがネットワークから取得できたら、バックグラウンドスレッドにてDBに保存
- 呼び出し元にリクエストの完了(及び
Loading
状態ではなくなったこと)を通知 - リクエストが失敗したときは、呼び出し元にエラー(及び
Loading
状態ではなくなったこと)を通知
MainViewModel.kt
を開いて、このAPIがどのようにUIの表示制御に使われているか見てみましょう。
fun refreshTitle() {
// stateリスナーをrefreshTitleにラムダで渡す
repository.refreshTitle { state ->
when (state) {
is Loading -> _spinner.postValue(true)
is Success -> _spinner.postValue(false)
is Error -> {
_spinner.postValue(false)
_snackBar.postValue(state.error.message)
}
}
}
}
refreshTitleを呼び出す側のコードは、それほど複雑ではありません。repository.refreshTitle
にコールバックを渡し、Loading
, Success
, Error
のいずれかの状態に変わるたびに繰り返し呼び出されます。どのケースでも、適切なLiveDataによりUIが更新されます。
Replace callback code with coroutines in TitleRepository(TitleRepositoryのコールバック実装をコルーチンに変更する)
TitleRepository.kt
を開き、refreshTitle
をコルーチンを用いた書き方に変更します。RefreshState
やTitleStateListener
はもはや使われないので、削除出来ます。
suspend fun refreshTitle() {
withContext(Dispatchers.IO) {
try {
val result = network.fetchNewWelcome().await()
titleDao.insertTitle(Title(result))
} catch (error: FakeNetworkException) {
throw TitleRefreshError(error)
}
}
}
このコードは前回fetchNewWelcome
をsuspend関数に変更したときに定義したawait
を使っています。await
はネットワークリクエストの結果をresume時に返しますから、コールバックを作る必要も無く、result
に直接代入することが出来ます。ネットワークリクエストがエラーになった場合は、await
は例外を投げます(resumeWithException
を呼んでるので)。従って、通常のtry/catchブロックで捉えることが出来るわけです。
withContext
関数は、データベースへの挿入処理がバックグラウンドスレッドで行われるようにするために使われています。insertTitle
は処理をブロックするため重要なことです。例えコルーチンの中で動いていても、処理が終わるまでそのコルーチンが走っているスレッドをブロックしてしまうのです。insertTitle
をメインスレッドから呼ぶと、例えコルーチンを使っていても、データベースの書込が終わるまで、UIがフリーズする原因となるでしょう。
withContext
を使うと、コルーチンは、指定されたディスパッチャーにブロック処理を受け渡します。ここで指定したDispatchers.IO
は、データベースなどのディスクIO向けに設計された巨大なスレッドプールです。withContext
が終了したとき、コルーチンはその前に指定されたディスパッチャーで処理を継続します。これはスレッドを短期的に切り替えて実行するとても良い例です。特にディスクIOやCPU集中タスク(大量の計算処理とかのこと?)等のメインスレッドから実行されるべきでない処理を行う場合に有効です。
ここではスコープの指定を必要としていません。なぜなら、ここではコルーチンを起動していないからです。この関数は呼び出し元のコルーチンのスコープにおいて実行されます。
新しいコードでは、ローディングステータスをもはや投げていないことに気付きましたでしょうか。MainViewModel
をこのsuspend関数を使うように変更したら、コルーチンでの実装においては明示的である必要が無いことが分かるでしょう。
Prefer suspend functions for Kotlin APIs(suspend関数を使う方が良い)
拡張関数await
を定義するのは、既存のAPI呼び出しをコルーチンに書き換えるときにはまあまあ良い方法です。でも、これはすべてのAPI呼び出しでawait
を使わなければならないという制約になります。(結局その書き換えが生じる)
新しくAPI呼び出しを作るなら、そもそもsuspend関数を直接使うべきです。
そうすれば全API呼び出しでawait
を呼び出さなくても良くなります。
Use suspend function in MainViewModel(MainViewModelでsuspend関数を使う)
MainViewModel.kt
を開き、refreshTitle
をコルーチンベースの実装に書き換えましょう。
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
この実装は通常のフロー制御でエラーをキャプチャしています。リポジトリクラスのrefreshTitle
はsuspend
関数なので、それが例外を投げれば、try/catchで捉えられるのです。
スピナーを表示するためのロジックも簡単です。refreshTitle
はrefreshが完了するまでコルーチンを中断するので、コールバックを通してステータスを渡す必要はありません。その代わり、ローディングスピナーのコントロールはViewModelのfinallyブロックで行われるようにすることが出来ます。
What happens to uncaught exceptions(未処理例外はどうなる?)
キャッチされていない例外はコルーチン以外での処理と同様、未処理例外ハンドラによってキャッチされます。コルーチンの処理はキャンセルされます。
アプリを実行してみましょう。画面のどこかをタップすると、スピナーが表示されるでしょう。タイトルがデータベースから更新されるか、あるいはエラーがスナックバーで表示されるでしょう。
次のエクササイズでは、これらのコードを汎用的なものにリファクタリングします。
10. Using coroutines in higher order functions(高階関数でのコルーチンの使用)
MainViewModel
のrefreshTitle
をリファクタリングし、汎用的にしていきます。コルーチンを使った高階関数*(※直訳)*の作り方が分かるようになります。
refreshTitle
は今の実装でも動きます。しかし、常にスピナーを表示する汎用的なデータ読み込み用のコルーチンを作ることも出来ます。これは、いくつかのイベントに応答してデータをロードし、ローディングスピナーが常に表示されるようにしたいような状況で役立ちます。
repository.refreshTitle()
の1行以外は、ボイラーテンプレートであって、スピナーを表示したりエラーを表示したりする部分は、いつも同じコードになることでしょう。
コードは割愛します
Important:重要
このコードラボでは、一つのスコープviewModelScopeしか使っていませんが、一般的には適宜必要なスコープを使うことが可能です。ただそれが不要になったときにキャンセルすることを忘れないでください。例えば、RecyclerViewのAdapterでDiffUtilを操作するとき等です。
Using coroutines in higher order functions(高階関数でコルーチンを使う)
MainViewModel.ktでlaunchDataLoadと実装しろというTODOを探してください。
コードは割愛します
そこを次のように置き換えます。
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
を、これを使うようにリファクタリングしましょう。
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
スピナーの表示やエラー表示を抽象化することで、実際のデータ周りのコードをシンプルにすることが出来ます。ローディングスピナーを表示したりエラーを表示したりする部分のコードは汎用化できることが多い一方、データはそのデータごとに読込や処置を実装しなければならないことがほとんどだからです。
抽象化するにあたり、launchDataLoad
はsuspendであるラムダを引数block
として受け取ります。suspendなラムダにより、suspend関数を呼ぶ事が出来ます。これが、Kotlinがこのコードラボで使用してきたコルーチンビルダーの起動とrunBlockingを実装している方法でもあります。
suspendラムダを使用するには、suspend
キーワードを使います。アロー演算子に続けて、戻り値としてUnitを宣言すれば完了です。
通常、suspendラムダを自分で定義していく必要はほとんど無いですが、このように何度も繰り返される処理をカプセル化するのにはとても有効な方法です。
次のエクササイズでは、WorkManagerからコルーチンを呼び出す方法について学びます。
11. Using coroutines with WorkManager(WorkManagerでコルーチンを使う)
WorkManagerからコルーチンベースの実装をどのように使うか学びます。
What is WorkManager(WorkManagerとは)
遅延可能なバックグラウンド処理を行う方法は、Androidにおいていくつかあります。ここでは、WorkManagerとコルーチンをどのように使うか見ていきましょう。WorkManagerは遅延可能なバックグラウンド処理向けの、互換性があり、柔軟でシンプルなライブラリです。WorkManagerは、Android上のこれらのユースケースに推奨されるソリューションです。
WorkManagerはAndroid Jetpackの一部であり、適切なタイミングでの実行が保証される必要のあるバックグラウンド処理向けのAACです。([opportunistic]の訳が大変難しいですが、「タイミングが来れば直ちに」とか、「機会が訪れればすぐに」、つまりスレッドが空けば、とか、リソースが空けば、みたいな意味かと思います。ネットの翻訳だと「日和見主義」ってなるけどどう考えても違うw) 適切なタイミングでの実行とは、WorkManagerはあなたのバックグラウンド処理を、可能な限り素早く行うことを指します。実行が保証されるということは、WorkerManagerがあなたのバックグラウンド処理を様々な状況下で開始させるための面倒を見てくれると言うことになります。たとえユーザーがあなたのアプリを離れていたとしてもです。
以上のことから、WorkManagerはいずれかならず成功すべきタスクにはとても良い選択肢です。
例えば以下のようなタスクは、WorkManagerに向いているでしょう。
- ログをアップロードする
- 画像にフィルターを適用したり保存したりする
- ローカルデータをネットワークと定期的に同期する
Using coroutines with WorkManager(WorkManagerでコルーチンを使う)
WorkManagerは、ListanableWorker
を基本とした、異なる実装を提供します。
最もシンプルなWorkerクラスは、いくつかの同期的な操作を行うことが出来ます。しかしながら、コルーチンとsuspend関数を使ったコードラボですから、WorkManagerを使う良い例はCoroutineWorker
を通してsuspend関数であるdoWork()
を定義して使うことでしょう。
class RefreshMainDataWork(context: Context, params: WorkerParameters) :
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(MainNetworkImpl, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
}
CoroutineWorker.doWork()
はsuspend関数であることに注目してください。単純なWorkerクラスと違い、このコードはWorkManager構成で指定されたExecutor上では実行されません。
※zipダウンロードした場合、プロジェクトが古いのか、上記コードをコピペしてもビルドが通りません。workライブラリのdependenciesの書き方が間違っているようので以下のように直してください。
def work_version = "2.0.1"
implementation "androidx.work:work-runtime-ktx:$work_version"
// optional - Test helpers
androidTestImplementation "androidx.work:work-testing:$work_version"
Gradle syncした後、CoroutineWorker
をimportして使えるようになります。
Testing our CoroutineWorker(CoroutineWorkerをテストする)
コードを実装したらテストを書いてこそ完成となります。
WorkManagerは、テスト方法がいくつかあります。詳しいことはドキュメントを読んで下さい。
WorkManagerのv2.1から、ListenableWorker
やCoroutineWorkerをテスト出来るシンプルなAPIセットが用意されました。ここではその新しいAPIのTestListenableWorkerBuilder
を使ってみましょう。
テストを追加するには、androidTestフォルダの下に、RefreshMainDataWorkTestという名前のKotlinファイルをまず作成します。ファイルのフルパスはこうなるはずです。
app/src/androidTest/java/com/example/android/kotlincoroutines/main/RefreshMainDataWorkTest.kt
ファイルの中身は次のようにします。
package com.example.android.kotlincoroutines.main
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.util.DefaultErrorDecisionStrategy
import com.example.android.kotlincoroutines.util.ErrorDecisionStrategy
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
private lateinit var context: Context
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
DefaultErrorDecisionStrategy.delegate =
object: ErrorDecisionStrategy {
override fun shouldError() = false
}
}
@Test
fun testRefreshMainDataWork() {
// Get the ListenableWorker
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context).build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result, `is`(Result.success()))
}
}
上記をコピペしただけだとビルド通りません。下記のようにdependenciesに変更、追加が必要です。
def work_version = "2.1.0-alpha02" // 変更
androidTestImplementation 'androidx.test:core:1.1.0' // 追加
setup関数では、テストが失敗しないようにデフォルトストラテジーを変更しています。(そうでなければランダムに通信は失敗します)(※これはこのプロジェクトで作っているものです。アプリを実行するとランダムに通信が失敗するようになっているので、これを無効化するためのコードです)
テストそのものは、TestListenableWorkerBuilder
を使ってworkerを作り、startWork()
メソッドを呼び出しています。
WorkManagerはコルーチンがAPI設計をいかにシンプルに出来るかの一例です。
12. Where to learn more(もっと学ぶには)
概要のまとめと発展的内容なので割愛します。
最後に
感想
ちょっと私が知りたかったこととは違った感じです。
プロジェクトの原形が、私には見慣れない形だったのでまずそっちを理解するのに消耗してしまいました。
Androidで標準とされてきた非同期処理の、AsyncTaskやAsyncTaskLoaderからの移植をしたいのでこのコードラボを見始めたのですが・・・
それとも、「標準」と思ってきたのは私だけで、世間では違ったのでしょうか?AsyncTask/AsyncTaskLoaderはGoogleさんにとってはそれほどまでに黒歴史なんでしょうか(笑)
何はともあれ、ここの情報を参考に頑張ってみます。
まあ、AsyncTask/AsyncTaskLoaderもコールバックベースではあるので、まるきり参考にならないと言うことはないでしょうが・・・・
でも、そもそもコルーチンに置き換えるならば、コールバック関数なんかも無くしたいので、やっぱりこのままは参考にならないかな。
ひとまずは、suspend functionを使えばいいのだと言うことは分かりました。
あと、とにかくテストせよ、と書いているのが興味深いですね。通信や重い処理の絡むテストは、確かに悩みどころではありますので、そこがこんなに簡単になるよ!というアピールなのでしょう。
AsyncTask/AsyncTaskLoader→コルーチンの書き換え、というかコールバックを使わないシンプルなAPIの形での書き換えが上手くいったら、別の記事でUPしたいと思います。
(コルーチン、非同期処理、で検索すると出てくる情報は、まだまだバージョンが古かったり、コルーチンがexperimentalの頃の物だったりしてだいぶ違うようなので、それで難儀しているのですが)