このコードラボでは、AndroidアプリでKotlin Coroutinesを使用する方法を学びます。バックグラウンドスレッドを管理する新しい方法で、コールバックの使用を最小限に抑えることでコードを簡素化することができます。Coroutinesは、データベースやネットワーク通信などの長時間実行タスクの非同期なコールバックを書かれた順番の通りに実行されるコードに変換するKotlinの機能です。
以下はこれから行うことの概要を示すコードスニペットです。
// 非同期なコールバック
networkRequest { result ->
// ネットワークリクエストが成功
databaseSave(result) { rows ->
// 結果の保存が完了
}
}
Coroutinesを使うことでコールバックを使ったコードをシーケンシャルなコードに変換することができます。
// Coroutinesを使用した同じコード
val result = networkRequest()
// 成功したネットワーク要求
databaseSave(result)
//結果を保存しました
最初はアーキテクチャ コンポーネントを使用して構築された、既存のアプリがある状態から始めます。このアプリでは長時間実行タスクにコールバックを使用しています。
このコードラボが終わる頃には、既存のAPIをCoroutinesを使用したものへの変換や、Coroutinesのアプリへの組み込みができるようになっていることでしょう。また、Coroutinesのベストプラクティスと、Coroutinesを使用するコードに対するテストの書き方についても理解できていることでしょう。
launch
やrunBlocking
を使用して、コードの実行を制御する方法suspendCoroutine
を使用して既存のAPIをCoroutinesに変換する手法以下のリンクからこのコードラボで必要なコードすべてをダウンロードすることができます。
または、以下のコマンドでコマンドラインからGitHubリポジトリをcloneすることもできます。
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
kotlin-coroutines リポジトリは3つの異なるapp projectsで構成されています:
まずはそのままの状態でサンプルアプリがどういう構造になっているかをみてみましょう。次の手順に従ってAndroid Studioでサンプルアプリを開きます。
kotlin-coroutines
zipファイルをダウンロードしている場合は、ファイルを展開しますkotlin-coroutines-start
プロジェクトを開きますこのサンプルアプリは、画面のどこかをタップするとスレッドを使用して1秒後にSnackbarを表示します。実際に試してみると、少し間をおいて「Hello, from threads!」と表示されるはずです。このコードラボの最初の部分では、このアプリをCoroutinesを使用したものに置き換えます。
このアプリではアーキテクチャーコンポーネントを使ってMainActivity
内のUI用コードとMainViewModel
のアプリケーションロジックを分離させています。一度プロジェクトの構造をみてみましょう。
MainActivity
がUIの表示や、クリックリスナの登録、Snackbarの表示を行います。MainViewModel
にイベントを渡し、MainViewModel
のLiveData
をもとに画面を更新します。MainViewModel
はonMainViewClicked
でイベントをハンドリングし、LiveData
を使ってMainActivity
に通達します。Executors
はバックグラウンドスレッドでコードを実行することができるBACKGROUND
を定義します。MainViewModelTest
はMainViewModel
のテストを定義します。KotlinでCoroutinesを使用するには、プロジェクトの build.gradle (Module: app)
ファイルに coroutines-core
ライブラリを含める必要があります。このコードラボのプロジェクトにはすでに含まれているので、コードラボを行うにあたって改めて追加する必要はありません。
Androidで使うCoroutinesはcoreライブラリとAndroid用の拡張関数として用意されています:
サンプルアプリのbuild.gradle
にはすでにdependenciesがincludeされています。新しくプロジェクトを作成する際には、build.gradle (Module: app)
を開いてcoroutines dependenciesを追加する必要があります。
dependencies {
...
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}
コードベースにRxJavaを使っている場合は、kotlin-coroutines-rxを使ってRxJavaをcoroutinesと一緒に使うことができます。
Androidではメインスレッドをブロックしないことが非常に重要です。メインスレッドはすべてのUIの更新を管理する単一のスレッドで、クリックハンドラなどUIからのすべてのコールバックが呼ばれるスレッドでもあります。つまり、最高のユーザーエクスペリエンスを保証するためにはメインスレッドを円滑に動かすことが必須なのです。
アプリがカクつかずに動作するには、メインスレッドは16ms以下(毎秒約60フレーム)の間隔で画面を更新する必要があります。大きなJSONデータセットをパースする、データベースにデータを書き込む、ネットワークからデータを取得するといった多くのタスクは16ms以上かかります。そういったコードをメインスレッドから呼ぶとアプリは止まったりカクついたり、最悪の場合フリーズしてしまうことがあります。そしてメインスレッドを長時間ブロックしてしまうと、アプリケーションが応答していません ダイアログと共にアプリはクラッシュしてしまうこともあるでしょう。
Coroutinesでメインセーフティを導入することによってこの問題がどのように解決されるかについては、 この動画 でも紹介しています。
コールバックはメインスレッドをブロックせずに長時間実行タスクを処理するパターンのひとつです。コールバックを使うことによって、background threadで長時間実行タスクを開始することができます。タスクが完了すると、メインスレッドでコールバックが呼ばれ結果が通知されます。
コールバックパターンの例を見てみましょう。
// コールバックを使った時間のかかるリクエスト
@UiThread
fun makeNetworkRequest() {
// 時間のかかるネットワークリクエストは別のスレッドで実行される
slowFetch { result ->
// 結果の準備ができると、このcallbackで結果を取得できる
show(result)
}
// slowFetchが呼ばれると、結果を待たずにmakeNetworkRequest()は終了する
}
このコードは
@UiThreadアノテーション(注釈)が付いているので、メインスレッド上で実行できるくらいに高速に動作しなければなりません。つまり、その後の画面の更新が遅れないよう、非常に素早く処理を終わらせる必要があります。しかしslowFetch
が完了するのに数秒かかるため、main threadでは結果が返ってくるのを待つことができません。show(result)
コールバックを使うことによって、background threadでslowFetch
を実行し、準備ができたら結果を返すことが可能になります。
コールバックは優れたパターンですが、いくつかの欠点があります。コールバックを頻繁に使用するコードは読みにくく、理解するのが難しくなります。またコールバックでは、例外などの一部の言語機能が使用できません。
Kotlin Coroutinesを使用すると、コールバックを使ったコードを同期的なコードに変換することができます。一般に同期的なコードは可読性が高く、例外などの言語機能も使用することができます。
コールバックもCoroutinesのどちらも「長時間実行タスクの結果を待って実行を再開する」という同じことを行いますが、コードの見た目は大きく異なります。
suspend
キーワードは、Kotlinにおいて関数や関数タイプがCoroutinesで使用できるように宣言する方法です。Coroutinesでsuspend
のついた関数を呼ぶと、通常の関数のようにその関数がreturnするまでスレッドをブロックするのではなく、結果の用意ができるまで処理をsuspend(中断)し、結果を取得するとその場所から処理をresume(再開)します。結果を待ってる間は、他の関数やCoroutinesが実行できるように実行中のスレッドをブロックするのをやめます。
以下の例では、makeNetworkRequest()
とslowFetch()
がsuspend関数になっています。
// Coroutinesを使った時間のかかるリクエスト
@UiThread
suspend fun makeNetworkRequest() {
// slowFetchは別の中断関数なので
// makeNetworkRequestは結果の準備ができるまで
// メインスレッドをブロックせずに`中断`する
val result = slowFetch()
// 結果の準備ができたら処理を再開する
show(result)
}
// Coroutinesを使っているslowFetchはメインセーフ
suspend fun slowFetch(): SlowResult { ... }
コールバックを使ったコードと同様に、makeNetworkRequest
は@UiThread
と表記されているのでメインスレッドを止めないようにすぐにreturnする必要があります。これでは通常slowFetch
のようなスレッドをブロックするメソッドを呼ぶことができません。そこでsuspend
キーワードの出番という訳です。
重要:suspend
キーワードは、コードが実行されるスレッドを指定するものではありません。中断関数はバックグラウンドスレッド、メインスレッドのどちらでも実行することができます。
Coroutinesを使ったコードは、コールバックを使ったコードと比較してより少ないコードで実行スレッドのブロックを回避することを実現できます。シーケンシャルな書き方のおかげで、複数のコールバックを作らずに長時間実行タスクをいくつも続けて実行することができます。例えば、2つのエンドポイントから結果を取得し、データベースに書き込むコードならCoroutinesを使った関数としてコールバックなしで以下のように書くことができます。
// Coroutinesを使ってネットワークからデータを要求し、データベースに保存する
// @WorkerThreadがついているので、
// メインスレッドでこの関数を呼ぶとエラーが発生する
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetchとanotherFetchは中断関数
val slow = slowFetch()
val another = anotherFetch()
// saveは通常の関数なのでこのスレッドをブロックする
database.save(slow, another)
}
// Coroutinesを使っているslowFetchはメインセーフ
suspend fun slowFetch(): SlowResult { ... }
// Coroutinesを使っているanotherFetchはメインセーフ
suspend fun anotherFetch(): AnotherResult { ... }
他の言語におけるasync
とawait
パターンはCoroutinesに基づいて作られています。このパターンで例えると、suspend
はasync
と似ているといえます。ただしKotlinではsuspend
関数を呼ぶと暗黙的にawait()
相当の待機状態になります。
またKotlinには、async
ビルダーで生成されたCoroutinesの結果を待つのに使えるDeferred.await()
メソッドもあります。
次はstart sample appをCoroutinesを使ったものに置き換えていきます。
この演習では、遅延後にメッセージを表示するcoroutineを作成します。はじめるにあたって、Android Studioでプロジェクトkotlin-coroutines-start
を開いていることを確認してください。
Kotlinでは、すべてのcoroutineは
CoroutineScopeの中で実行されます。Scopeは、jobを通じてcoroutineの生存期間を制御します。Scopeのjobをキャンセルすると、そのスコープの中で開始されたすべてのcoroutineがキャンセルされます。Androidでは、ActivityやFragmentから別画面に遷移した時などにスコープを使って実行中のcoroutineをまとめてキャンセルすることができます。またscopeには規定のdispatcherの指定することもできます。dispatcherはcoroutineがどのスレッドで実行されるかを制御します。
MainViewModel.kt
でcoroutineを使うには、scopeを次のように作成します。
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
この例では、uiScope
はDispatchers.Main
(Androidのメインスレッド)でcoroutineを実行します。Dispatchers.Main
で実行されるcoroutineは、中断中はメインスレッドをブロックしません。ViewModel
で使うcoroutineはほとんどの場合メインスレッドでUIを更新するため、メインスレッドでcoroutineを開始するのが妥当でしょう。コードラボの後のほうでも出てきますが、main dispatcherで始まったcoroutineが別のdispatcherを使って大きなJSONをパースし結果をメインスレッドで扱うといったように、coroutineは開始後いつでもdispatcherを切り替えて実行することができます。
CoroutineScopeは、パラメーターとしてCoroutineContext
を受け取ることができます。CoroutineContext
はcoroutineを設定するためのattribute setです。スレッドポリシーやexception handlerなどを定義することができます。
上記の例では、CoroutineContext
のプラス演算子を使ってスレッドポリシー(
Dispatchers.Main)とjob(viewModelJob
)を定義しています。演算結果のCoroutineContext
は両方のcontextを合わせたものになります。
ViewModel
が使用されなくなり破棄されると、onCleared
が呼び出されます。これは通常、ユーザーがViewModel
を使用していたアクティビティやフラグメントから遷移したときに発生します。前の章で作成したscopeをキャンセルするには、次のコードを含める必要があります。
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
viewModelJob
はuiScope
のjobとして渡されるため、viewModelJob
がキャンセルされると、uiScope
によって開始されたすべてのcoroutineもキャンセルされます。無駄ななタスクの実行やメモリリークを回避するために、不要になったcoroutineはキャンセルすることが重要です。
重要: scopeで開始されたすべてのcoroutineをキャンセルするには、 CoroutineScope
にJob
を渡す必要があります。そうしないとアプリが終了するまでscopeが実行されてしまい、意図しない挙動の場合メモリリークにつながってしまいます。
CoroutineScope
コンストラクタで作成されたスコープは暗黙的なjobを追加します。このjobはuiScope.coroutineContext.cancel()
でキャンセルできます。
上記のコードはプロジェクト内すべてのViewModel
に含めてscopeを結びつけることができますが、大量のボイラープレートが追加されてしまいます。そこでAndroidXのlifecycle-viewmodel-ktx
ライブラリの登場です。このライブラリを使用するには、プロジェクトのbuild.gradle (Module: app)
ファイルに含める必要があります。今回のコードラボプロジェクトにはすでに追加されています。
dependencies {
...
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}
このライブラリを導入することでViewModel
クラスにviewModelScope
が拡張関数として追加されます。このscopeはDispatchers.Main
にbindされていて、ViewModel
がclearされるときに自動的にキャンセルされます。ViewModel
を作る度にいちいち新しいscopeを作らなくても、viewModelScope
を使うだけでライブラリが自動的に開始と終了処理を行ってくれます。
viewModelScope
を使うと以下のようにbackground threadでネットワークリクエストを行うcoroutineを開始できます。
class MainViewModel : ViewModel() {
// UIスレッドをブロックせずにネットワークリクエストを行う
private fun makeNetworkRequest() {
// viewModelScope内でcoroutineを開始する
viewModelScope.launch(Dispatchers.IO){
// slowFetch()
}
}
// onCleared()をオーバーライドする必要がない
}
MainViewModel.kt
には、TODOとともに以下のようなコードがあります:
/**
* 1秒待ってsnackbarを表示する
*/
fun onMainViewClicked() {
// TODO: Coroutinesを使った実装に置き換える
BACKGROUND.submit {
Thread.sleep(1_000)
// バックグラウンドスレッドでの実行なのでpostValueを使う
_snackBar.postValue("Hello, from threads!")
}
}
ここではBACKGROUND
を使ってbackground threadでコードを実行しています。sleep
は現在のスレッドをブロックするので、メインスレッドで呼ばれるとUIが止まってしまいます。ユーザーがmain viewをクリックした1秒後にsnacbarが呼ばれます。
onMainViewClicked
をCoroutinesを使ったコードに置き換えてみましょう。launch
とdelay
をimportする必要があります。
/**
* 1秒待ってsnackbarを表示する
*/
fun onMainViewClicked() {
// viewModelScope内でcoroutineを開始する
viewModelScope.launch {
// このcoroutineを1秒間中断する
delay(1_000)
// main dispatcherで再開する
// _snackbar.value はメインスレッドから直接呼べる
_snackBar.value = "Hello, from coroutines!"
}
}
このコードも1秒待ってsnackbarを表示しますが、いくつか重要な違いがあります
viewModelScope.launch
はviewModelScope
でcoroutineを開始します。これは、viewModelScope
に渡したジョブがキャンセルされると、このジョブまたはスコープ内のすべてのcoroutineがキャンセルされることを意味します。delay
が返される前にユーザーがアクティビティを離れた場合、ViewModelを破棄する際にonCleared
が呼び出されると、このcoroutineは自動的にキャンセルされます。viewModelScope
の既定のdispatcherはDispatchers.Main
なので、このcoroutineはメインスレッドでlaunchされます。他のスレッドを使う方法については後述されます。delay
はsuspend
関数です。これは、Android Studio左側にあるアイコンによって示されます。このcoroutineはメインスレッドで実行されますが、delay
はスレッドを1秒間ブロックする訳ではありません。代わりに、1秒後にcoroutineが次のstatementで再開するようにdispatcherが予約をします。実行してみましょう。画面をタップすると、1秒後にスナックバーが表示されるはずです。
次の章では、この関数をテストする方法をみていきます。
この章では、書いたコードのテストを作成します。この章では、スレッドを使用したコードのテストと同じようにCoroutinesのテストの書き方を紹介します。後半では、Coroutinesと直接interactするテストを書いてみます。
kotlinx-coroutines-testライブラリが最近リリースされ、AndroidにおけるCoroutinesのテストを簡素化するための多くのutilityが提供されています。ライブラリは現在@ExperimentalCoroutinesApi
状態であり、最終リリース前に変更が入る可能性があります。
ライブラリは、端末外でテストを実行するときにDispatchers.Mainを設定する方法やテストコードでcoroutineの実行をコントロールするtesting dispatcherを提供します。
これにより以下が可能になります。
詳細については、kotlinx-coroutines-testのドキュメントを参照してください。
ライブラリは現在experimentalとされているため、このコードラボでは安定するまで既存のAPIを使用してテストを書く方法を紹介します。
kotlinx-coroutines-test
を使用して書き換えられたテストコードはこの章の最後に記載します。
androidTest
フォルダのMainViewModelTest.kt
を開きます。
@RunWith(JUnit4::class)
class MainViewModelTest {
/**
* このテストでは、LiveDataはスレッドを変えずにすぐに値をpostします。
*/
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
/**
* テストを開始する前にsubjectを初期化する
*/
@Before
fun setup() {
subject = MainViewModel()
}
}
それぞれのテストが始まる前に2つのことが起こります。
InstantTaskExecutorRule
は、テストの実行中にすぐにメインスレッドにpostするようにLiveDataを構成するJUnitルールです。setup()
の中でsubject
フィールドは新しいMainViewModel
として初期化されます。このセットアップ後にはテストが1つ定義されています。
@Test
fun whenMainViewModelClicked_showSnackbar() {
runBlocking {
subject.snackbar.captureValues {
subject.onMainViewClicked()
assertSendsValues(2_000, "Hello, from threads!")
}
}
}
このテストはonMainViewClicked
を呼び出し、helperのassertSendsValues
を使用してスナックバーを待機します。このhelperは、値が LiveData
に送信されるまで最大で2秒間待機します。この関数の中身を読まなくてもコードラボの完遂に支障はありません。
このテストはViewModel
のパブリックAPIにのみ依存します。onMainViewClicked
が呼び出されると、"Hello, from threads!"がスナックバーに渡されます。
パブリックAPIには変更を加えませんでした。メソッド呼び出しは通常通りスナックバーを更新するため、Coroutinesを使う実装を変更してもテストは壊れることはありません。
MainViewModelTest
を右クリックして、コンテキストメニューを開きます。前の章で実装したコードでテストを実行すると、assertion failureが発生します。
expected: Hello, from threads!
but was : Hello, from coroutines!
"Hello, from threads!"という出力を"Hello, from coroutines!"に変更したので、このテストは失敗します。
Assertionを変更して、テストを新しい挙動に対応させます。
@Test
fun whenMainViewModelClicked_showSnackbar() {
runBlocking {
subject.snackbar.captureValues {
subject.onMainViewClicked()
assertSendsValues(2_000, "Hello, from coroutines!")
}
}
}
ツールバーのを使用してもう一度テストを実行すると、テストが通ります。
パブリックAPIに対してのみテストを行うことにより、テストの構造を変更することなく、バックグラウンドスレッドからCoroutinesに変更することができました。
次の章では、既存のコールバックを使用したAPIをCoroutinesに変換する方法について確認します。
このテストにはまだ大きな問題が1つ残っています。delay(1_000)
が onMainViewClicked
にハードコードされているので、実行にまるまる1秒もかかるのです!
テストは可能な限り速く実行されるべきですし、このテストは間違いなくより速く実行させることができます。kotlinx-coroutines-test
が提供するTestCoroutineDispatcher
を使用すると、「仮想時間」を制御して、実際には1秒待機することなく1秒遅延の関数を呼び出すことができます。
以下はまだExperimentalなTestCoroutineDispatcherを使ってこの章のテストを書き換えたものです。
/**
* 同じテストをexperimentalなkotlinx-coroutines-test API
* で書いた例
*/
@RunWith(JUnit4::class)
class MainViewModelTest {
/**
* このテストでは、LiveDataはスレッドを変えずにすぐに値をpostする
*/
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
/**
* このDispatcherにより、テストの時間が進行
*/
var testDispatcher = TestCoroutineDispatcher()
lateinit var subject: MainViewModel
/**
* テストを開始する前にsubjectを初期化
*/
@Before
fun setup() {
// Dispatchers.Mainをセット
// 端末外でテストを実行できるようになる
Dispatchers.setMain(testDispatcher)
subject = MainViewModel()
}
@After
fun teardown() {
// テストが終わったらmainをリセット
Dispatchers.resetMain()
// TestCoroutineDispatcherが次のテストで状態を
// 保持してしまわないように以下を呼んでおく
dispatcher.cleanupTestCoroutines()
}
// runBlockingの代わりにrunBlockingTestの使用していることに注意
// これにより時間の制御が可能になります
@Test
fun whenMainViewModelClicked_showSnackbar() = testDispatcher.runBlockingTest {
subject.snackbar.observeForTesting {
subject.onMainViewClicked()
// 1秒間進める
advanceTimeBy(1_000)
// 値は待たずにすぐに利用可能
Truth.assertThat(subject.snackbar.value)
.isEqualTo("Hello, from coroutines!")
}
}
// LiveDataから値を取得できるようにするヘルパーメソッド
// LiveDataはobserverが1つ以上になるまで結果をpublishしない
private fun <T> LiveData<T>.observeForTesting(
block: () -> Unit) {
val observer = Observer<T> { Unit }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
}
この章では、コールバックを使った既存のAPIをCoroutinesを使ったものに変換します。
Android Studioでkotlin-coroutines-repository
のプロジェクトを開いてみましょう。
このアプリはアーキテクチャコンポーネントを使用して、ネットワークとローカルデータベースの両方を使用するデータレイヤーを、前章のプロジェクトに付け加える形で実装しています。メインビューがクリックされると、ネットワークから取得した新しいタイトルをデータベースに保存して、画面に表示します。少し時間をとって、新しいクラスに慣れましょう。
MainDatabase
には、Roomを使用してTitle
を読み書きするデータベースが実装されています。MainNetwork
には、新しいタイトルを取得するネットワークAPIを実装します。FakeNetworkLibrary.kt
で定義された偽のネットワークライブラリを使用してタイトルを取得します。ネットワークライブラリはランダムにエラーを返します。TitleRepository
は、ネットワークとデータベースからのデータを組み合わせてタイトルを取得または更新するための単一のAPIを実装します。MainViewModelTest
はMainViewModel
のテストを定義します。FakeNetworkCallAwaitTest
は、このコードラボで後ほど完了するテストです。MainNetwork.kt
を開いてfetchNewWelcome()
を確認してみましょう
// MainNetwork.kt
fun fetchNewWelcome(): FakeNetworkCall<String>
TitleRepository.kt
を開いて、 fetchNewWelcome()
でコールバックを使ってネットワークコールを行っていることを確認しましょう。
この関数の返すFakeNetworkCall
を使って、呼び出し元はリクエストに対するリスナを登録することができます。fetchNewWelcome
を呼び出すと、ネットワークへのリクエストが長時間実行タスクとして開始され、同時にaddOnResultListener
を公開するオブジェクトを呼び出し元に返します。リクエストの完了やエラー時にコードを実行できるように、このコードではaddOnResultListener
にコールバックを渡しています。
// TitleRepository.kt
fun refreshTitle(/* ... */) {
val call = network.fetchNewWelcome()
call.addOnResultListener { result ->
// ネットワークリクエストが完了するかエラーが発生すると呼ばれるコールバック
when (result) {
is FakeNetworkSuccess<String> -> {
// 成功時の結果を処理
}
is FakeNetworkError -> {
// エラー時の結果を処理
}
}
}
}
refreshTitle
はFakeNetworkCall
のコールバックを使用して実装されています。 この章では、refreshTitle
をcoroutineとして書き換えられるようにネットワークAPIを中断関数として公開することを目指します。
Kotlinでは、suspendCoroutine
を使ってコールバックを使ったAPIを中断関数に置き換えることができます。
suspendCoroutine
を呼ぶことで現在のcoroutineがすぐに中断されます。suspendCoroutine
を使うとcoroutineを再開するのに必要なcontinuation
オブジェクトを取得することができます。 continuation
は言葉通りcoroutineを「continue」(継続、再開)するのに必要なcontextを保持します。
suspendCoroutine
が提供するcontinuation
にはresume
とresumeWithException
の2つの関数があります。いずれかの関数を呼び出すと、suspendCoroutine
がすぐに再開されます。
suspendCoroutine
を使用すると、コールバックを待つ前にcoroutineを中断させることができます。 その後コールバックが呼び出される際に、resume
かresumeWithException
を呼ぶとコールバック結果を持ってcoroutineを再開させることができます。
以下はsuspendCoroutine
の例です。
// suspendCoroutineの例
/**
* 任意の文字列をコールバックに渡すクラス
*/
class Call {
fun addCallback(callback: (String) -> Unit)
}
/**
* coroutine内で使えるように、中断関数としてコールバックを使ったAPIを公開
*/
suspend fun convertToSuspend(call: Call): String {
// 1: suspendCoroutineを呼んでcoroutineをすぐに*中断*
// ブロックに渡されるcontinuationオブジェクトを使ってのみ
// *再開*することができる
return suspendCoroutine { continuation ->
// 2: コールバックを登録するためにブロックをsuspendCoroutineに渡す
// 3: コールバックを追加して結果を待つ
call.addCallback { value ->
// 4: continuation.resumeで値を渡して
// coroutineを*再開*。resumeに渡された値は
// suspendCoroutineの結果となる
continuation.resume(value)
}
}
}
この例ではsuspendCoroutine
を使用して、Call
のコールバックを使ったAPIを中断関数に変換する方法を示しています。Coroutinesを使ったコードでCall
を直接使用できるようになりました。
// convertToSuspendを活用してコールバックを使ったAPIをCoroutinesで使用する例
suspend fun exampleUsage() {
val call = makeLongRunningCall()
convertToSuspend(call) // 長時間実行コールが完了するまで中断する
}
このパターンを使用して、FakeNetworkCall
の中断関数を公開してみましょう。公開することでコールバックを使ったネットワークAPIをCoroutinesで使用できるようになります。
suspendCoroutineはcoroutineをキャンセルする必要がない場合に適しています。ただ、通常はキャンセルについて考慮する必要があり、その場合は
suspendCancellableCoroutineが適しています。これを使用することでコールバックを使ったAPIへのキャンセルをサポートするライブラリにキャンセルをでんぱさせることが可能になります。
TitleRepository.kt
の一番下までスクロールして、拡張機能を実装するTODOコメントを探しましょう。
/**
* コールバックを使った[FakeNetworkCall]をCoroutinesで使用するための中断関数
*
* @return 通信完了後の結果
* @throws Throwable 通信が失敗した場合に発生するライブラリからの例外
*/
// TODO: FakeNetworkCall<T>.await()をここに実装
このTODOをFakeNetworkCallに対する以下の拡張関数に置き換えます
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)
}
}
}
}
この拡張関数はsuspendCoroutine
を使用して、コールバックを使ったAPIを中断関数に変換します。await
を呼ぶことで、通信結果の準備ができるまですぐにcoroutineを中断させることができます。通信の結果はawait
の戻り値となり、エラーが発生した際は例外を投げます。
次のように使用されます。
// awaitの使用例
suspend fun exampleAwaitUsage() {
try {
val call = network.fetchNewWelcome()
// fetchNewWelcomeが結果を返すかエラーを投げるまで中断
val result = call.await()
// resumeでawaitは通信結果を返す
} catch (error: FakeNetworkException) {
// resumeWithExceptionでawaitはエラーを投げる
}
}
await
の関数のシグネチャを読んでみましょう。suspend
キーワードがつくので、Kotlinはこの関数がCoroutinesで利用できるものと解釈します。その結果、suspendCoroutine
などの他の中断関数からこの関数を呼び出すことができるようになっています。残りのfun <T> FakeNetworkCall<T>.await()
部分は、どんなFakeNetworkCall
でも呼べる拡張関数await
を定義します。実際のクラスは変更されません変が、Kotlinではパブリックメソッドとして呼び出すことができます。await
の戻り値の型は、関数名の後に指定されるT
です。
Kotlinに慣れてない人にとっては、拡張関数は新しい概念かもしれません。拡張関数はクラスを変更せず、代わりに this
を最初の引数として取る新しい関数を宣言します。
fun <T> await(this: FakeNetworkCall<T>): T
await
関数の中では、thisは渡されてきたFakeNetworkCall<T>
となります。await
はメンバーメソッドと同じように暗黙的なthisを使って、addOnResultListener
を呼び出します。
つまり、このシグネチャはawait
というsuspend
関数を、元々Coroutinesを考慮して作られていないクラスに追加することを意味しています。つまりこのアプローチを使うことで、コールバックを使ったAPIの実装を変更せずにCoroutinesへの対応を追加することが可能になります。
次の章では、await()
のテストの書き方や、テストから直接Coroutinesを呼び出す方法について取り上げます。
この章では、suspend
関数を直接呼ぶテストを作成します。
await
はパブリックAPIとして公開されているので、直接テストしてみて、テストからCoroutines関数を呼ぶ方法を確認してみましょう。
前章で実装したawait関数は以下のとおりです。
// TitleRepository.kt
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)
}
}
}
}
androidTest
フォルダにあるFakeNetworkCallAwaitTest.kt
を開いてみましょう。2つのTODOコメントがあるはずです。
2番目のテスト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
関数であるため、Coroutinesか別の中断関数以外からの呼び出す方法がありません。_"Suspend function ‘await' should be called only from a coroutine or another suspend function."_といった内容のコンパイラエラーが表示されるはずです。
テストランナーはCoroutinesについては何も知らないため、このテストを中断関数にすることはできません。ViewModel
の中でのようにCoroutineScope
を使用してcoroutineをlaunch
することもできますが、coroutineが完了するまでテストの終了を待たなくてはなりません。テスト関数が戻ると、テストは終了してしまいます。「launch」で始まるcoroutineは非同期に実行されるコードであり、未来のどこかで完了します。したがって非同期コードをテストするには、coroutineが完了するまで待機するようテストに指示する方法が必要です。launch
は、テストでは使用できません。関数が値を返したあともcoroutineを実行し続けるのにスレッドをブロックせずにすぐに終了してしまうためです。例えば、以下のようなコードがあります。
// テストでlaunchを使用する例(絶対に失敗する)
@Test(expected = FakeNetworkException::class)
fun whenFakeNetworkCallFailure_throws() {
val subject = makeFailureCall(FakeNetworkException("the error"))
// launchでcoroutineを開始し、すぐに終了する
GlobalScope.launch {
// 非同期的に実行されるコードなので、テストの*完了後*に呼ばれる
subject.await()
}
// テスト関数はすぐに終了するので
// await()で発生する例外を検知しない
}
このテストは必ず失敗します。launch
への呼び出しはすぐに戻り、テストケースが終了します。await()
で発生するか例外は、テストの終了前にも終了後に発生する場合がありますが、テストコールスタックでは例外はスローされません。代わりに、scope
の例外ハンドラーにスローされます。
Kotlinには、中断関数を呼んでいる間スレッドをブロックするrunBlocking
関数があります。runBlocking
が中断関数を呼ぶと、スレッドを中断する代わりに通常の関数と同じでスレッドをブロックします。見方を変えると、中断関数を通常の関数呼び出しに変換する方法として捉えることもできます。
runBlocking
は通常の関数と同じようにcoroutineを実行するため、通常の関数と同じように例外もスローします。
重要:runBlocking
関数は、通常の関数呼び出しのように常に呼び出し元のスレッドをブロックします。coroutineは同じスレッドで同期的に実行されます。アプリケーションコードではrunBlocking
を避け、すぐに終了するlaunchを使用したほうがよいでしょう。
runBlocking
は、テストなどのスレッドをブロックすることを期待する場面でのみ使用するべきです。
最近リリースされた
kotlinx-coroutines-testライブラリは、runBlocking
の代わりにテスト用としてrunBlockingTest
を提供します。
ライブラリは現在experimentalとされているため、安定版がでるまではこのコードラボでは既存のAPIを使用してテストを書く方法を紹介します。
ライブラリを使用する場合は、runBlocking
が出現する部分を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")
}
}
テストを実行してみましょう。実行すると、すべて通るはずです!
次の章では、Coroutinesを使用してRepository
とViewModel
でデータを取得する方法について取り上げます。
この章では、「TitleRepository」の実装を完了するために、Coroutinesが実行されるスレッドを切り替える方法を取り上げます。
TitleRepository.kt
を開き、コールバックを使った既存の実装を確認しましょう。
// TitleRepository.kt
fun refreshTitle(onStateChanged: TitleStateListener) {
// 1: ネットワークリクエストの開始を通知
onStateChanged(Loading)
val call = network.fetchNewWelcome()
// 2: リクエストの結果が完了またはエラーになったときに通知を受け取るコールバックを登録
call.addOnResultListener { result ->
when (result) {
is FakeNetworkSuccess<String> -> {
// 3: バックグラウンドスレッドで新しいタイトルを保存
BACKGROUND.submit {
// バックグラウンドスレッドでinsertTitleを実行します
titleDao.insertTitle(Title(result.data))
}
// 4: 呼び出し元にリクエストが成功したことを伝える
onStateChanged(Success)
}
is FakeNetworkError -> {
// 5: 呼び出し元にリクエストにエラーが発生したことを伝える
onStateChanged(
Error(TitleRefreshError(result.error)))
}
}
}
}
TitleRepository.kt
のrefreshTitle
メソッドには、呼び出し元に完了とエラーの状態を通知するコールバックが実装されています。2つのコールバックが連動するため、コードが少し読みづらくなっています。詳しく見てみましょう。
Loading
であることが通知されるFakeNetworkCall
に別のコールバックが登録されるLoading
状態でなくなる)Loading
状態でなくなる)MainViewModel.kt
を開き、UIを制御するのにこのAPIをどう使用されているかを確認してみましょう。
// MainViewModel.kt
fun refreshTitle() {
// 状態リスナーをラムダとして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が更新されます。
TitleRepository.kt
を開いて、refreshTitle
をCoroutinesを使った実装に置き換えます。置き換える実装ではRefreshState
とTitleStateListener
は使用しないので、この段階で削除してしまいましょう。
// TitleRepository.kt
suspend fun refreshTitle() {
withContext(Dispatchers.IO) {
try {
val result = network.fetchNewWelcome().await()
titleDao.insertTitle(Title(result))
} catch (error: FakeNetworkException) {
throw TitleRefreshError(error)
}
}
}
// class RefreshStateとtypealias TitleStateListenerを削除する
このコードでは、先に定義したawait
関数を使用してfetchNewWelcome
をsuspend
関数に変換します。await
は再開時にネットワークリクエストの値を結果として返すため、コールバックを作成せずにresult
に直接結果を代入することができます。リクエストがエラーになると、await
は(resumeWithException
で呼んでいるため)例外を吐くので、通常通りtry/catchブロックで例外をキャッチすることができます。
withContext
関数は、データベースへの追加がバックグラウンドスレッドで実行されることを保証するために使用されます。insertTitle
はブロッキング関数なので、この指定は重要です。coroutineで実行されていても、終了までcoroutineが実行されるスレッドをブロックします。例えcoroutine内であっても、メインスレッドからinsertTitle
を呼ぶと、データベースの書き込み中にアプリがフリーズしてしまいます。
withContext
を使用すると、coroutineは渡されたブロックを指定されたディスパッチャーで実行します。ここではDispatchers.IO
を指定します。Dispatchers.IO
は、データベース書き込みなどのIO操作を処理するために特別に調整された大きなスレッドプールです。withContext
が終了すると、coroutineは直前に指定されていたディスパッチャーで処理を続行します。スレッドを短時間切り替えて、メインスレッドで実行すべきではないディスクIOやCPU負荷の高いタスクなど、時間のかかるタスクを実行するための良い方法です。
この中断関数はcoroutineを起動しないため、ここではスコープは必要ありません。呼び出し元coroutineのスコープで関数が実行されます。
このコードがロード状態を明示的に渡していないことに気づきましたか?このあとこの中断関数を呼ぶようにMainViewModel
を変更する時に、coroutineの実装内で明示的にする必要がないことがわかります。
ここで定義されている拡張機能のawait
は、既存のAPIをcoroutineにブリッジさせるには良い方法です。しかし、呼び出し側がawait
を呼び出すことを忘れないことに依存します。Kotlinで使用するAPIを新しく設計するときは、suspend
関数を通じて直接結果を返すほうが適切です。
suspend fun fetchNewWelcome: String
fetchNewWelcome
がこのような中断関数として再定義する場合、呼び出し元は使用するたびにawaitを呼び出すことを覚えておく必要はありません。
MainViewModel.kt
を開き、refreshTitle
をCoroutinesを使ったの実装に置き換えます。
// MainViewModel.kt
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
は更新が完了するまでcoroutineを中断させるため、コールバックを介してロード状態を明示的に渡す必要はありません。代わりに、スピナーは ViewModel
によって制御され、finallyブロックで非表示になります。
coroutineスコープのキャッチされない例外は、coroutineでない通常のコードのものに似ています。デフォルトではスコープに渡されたジョブはキャンセルされ、例外が uncaughtExceptionHandler
に渡されます。
CoroutineExceptionHandlerを使うことで、この挙動をカスタマイズすることができます。
app、と選択してアプリを再度実行すると、画面をタップするとスピナーが表示されることが確認できます。タイトルはRoomデータベースから更新されます。エラーの場合は、スナックバーが表示されます。
次の章では、このコードを汎用的なデータ読み込み関数を使用するようにリファクタリングします。
この章では、MainViewModel
のrefreshTitle
をリファクタリングして、MainViewModel
が汎用的なデータ読み込み関数を使用するようにします。リファクタリングすることによって、Coroutinesを使用する高階関数の書き方について学びんでいきます。
現状でもrefreshTitle
の実装は機能はしていますが、スピナーを常に表示するような、データ読み込み用の汎用的なcoroutineを作ることができます。これは複数のイベントに応答してデータを読み込み、スピナーが常に同じように表示されるような要件の場合に役立ちます。
repository.refreshTitle()
以外のすべての行はスピナーやエラーを表示するためのボイラープレートです。
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// 変更するのはこの部分のみ
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
重要: ここではviewModelScopeのみを使用していますが、適切な場所であればどこにスコープを追加しても構いません。不要になったらキャンセルすることを忘れないようにだけ注意してください。
たとえば、DiffUtil操作を実行するためにRecyclerView Adapter内に宣言することもできます。
MainViewModel.ktでlaunchDataLoadを実装するTODOコメントを探しましょう。
// MainViewModel.kt
/**
* スピナーを表示してデータ読み込み用関数を呼ぶするヘルパー関数
* エラーの場合はスナックバーを表示する
*
* `block`や`suspend`と記述することで
* 中断関数が呼べる中断ラムダを生成する
*
* @param block 実際にデータを読み込むラムダ。viewModelScope内で呼び出される。
* ラムダを呼び出す前にスピナーが表示され、
* 完了またはエラーの場合にスピナーを隠す
*/
// TODO: ここにlaunchDataLoadを追加して、refreshTitleで呼び出すようにリファクタリングを行う
このTODOを以下の実装に置き換えます
// 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
を引数として撮ります。中断ラムダを使用すると中断関数を呼び出すことができます。Kotlinはこのように、このコードラボでも使用してきたlaunchやrunBlockingといったcoroutineビルダーを実装しています。
// 中断ラムダ
block: suspend () -> Unit
中断ラムダを生成するには、suspend
キーワードを使用します。関数の矢印と戻り値の型Unitの宣言も必要です。
多くの場合、独自の中断ラムダを宣言する必要はありませんが、今回のように繰り返し使うロジックをカプセル化するには非常に有用です。
次の章では、WorkManagerからCoroutinesを使ったコードを呼び出す方法を取り上げます。
この章では、WorkManagerからCoroutinesを使ったコードを使用する方法を取り上げます。
Androidには、遅延可能なバックグラウンド作業を実行するための選択肢がいくつもあります。この章では、WorkManagerをCoroutinesで実装する方法を示します。WorkManagerは、遅延可能なバックグラウンド作業のための、互換性のある、柔軟でシンプルなライブラリです。WorkManagerは、Androidでこういったユースケースに対応する場合に推奨される手法です。
WorkManagerはAndroid Jetpackの一部であり、 機に便乗した実行(opportunistic execution)と保証される実行(guaranteed execution)の組み合わせが必要なバックグラウンド作業のためのアーキテクチャーコンポーネントです。機に便乗した実行とは、WorkManagerがバックグラウンドの作業をできるだけ早く行うことを意味します。保証される実行とは、ユーザーがアプリ外に遷移するなど、様々な状況下でWorkManagerが作業を開始するのに必要なロジックをこなすことを意味します。
このため、WorkManagerはいつか必ず完了する必要のあるタスクに適しています。
WorkManagerを適切に使用するタスクの例:
WorkManagerの詳細については、ドキュメントを参照ください。
WorkManagerはさまざまなユースケースに対応するため、基本となる ListanableWorker
クラスのさまざまな実装を提供しています。
WorkManagerによって同期的な操作が実行されるWorkerクラスを使うことが、WorkManagerを使う最も単純な方法です。しかし、これまでCoroutinesと中断関数を使用するようにコードベースを変換する作業を行ってきたので、doWork()
関数をサスペンド関数として定義できる CoroutineWorker
クラスを使用するのがよいでしょう。
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()
は中断関数であることに注意してください。より単純な Worker
クラスとは異なり、このコードはWorkManager設定で指定されたExecutorでは実行されません。
テストがなければ完全なコードベースとはいえません。
WorkManagerは、Worker
クラスをテストするためいくつかの異なる方法を提供します。元のテストインフラストラクチャの詳細については、ドキュメントを参照してください。
WorkManager v2.1では、ListenableWorker
クラスをテストしやすくするAPIが追加されていて、結果的にCoroutineWorkerもテストしやすくなっています。今回のコードでは、新しく追加された
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() {
// ListenableWorkerを取得
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context).build()
// 同期的にタスクを開始
val result = worker.startWork().get()
assertThat(result, `is`(Result.success()))
}
}
セットアップ用の関数では、シミュレートされたネットワーク接続の決定戦略を書き換えて絶対失敗しないようにします(でないと、デフォルトの設定したエラーしきい値でテストがたまに失敗しています)。
テスト自体はTestListenableWorkerBuilder
を使用してワーカーを作成し、startWork()
メソッドを呼んでワーカーを実行します。
WorkManagerは、Coroutinesを使用してAPI設計をシンプルにする方法の一例にすぎません。
このコードラボでは、アプリでCoroutinesの使い始めるための基礎について紹介しました。
非同期プログラミングを簡素化するために、UIとWorkManager両方の観点からAndroidアプリにCoroutinesを組み込む方法を取り上げました。またメインスレッドをブロックせずにネットワークからデータを取得しデータベースに保存する方法をして、ViewModel
内でCoroutinesを使用する方法を紹介しました。ViewModel
が終了したときにすべてのcoroutineをキャンセルする方法についても取り上げています。
Coroutinesを使ったコードをテストするために、挙動をテストすることとサ
スペンド関数を直接呼び出すことの両方について取り上げました。またsuspendCoroutine
を使用して、既存のコールバックを使ったAPIをcoroutineに変換する方法についても紹介しています。
KotlinにおけるCoroutinesには、まだこのコードラボではカバーしきれなかった多くの機能があります。詳細については、JetBrainsが公開しているcoroutines guidesを参照ください。