このコードラボでは、AndroidアプリでKotlin Coroutinesを使用する方法を学びます。バックグラウンドスレッドを管理する新しい方法で、コールバックの使用を最小限に抑えることでコードを簡素化することができます。Coroutinesは、データベースやネットワーク通信などの長時間実行タスクの非同期なコールバックを書かれた順番の通りに実行されるコードに変換するKotlinの機能です。

以下はこれから行うことの概要を示すコードスニペットです。

// 非同期なコールバック
networkRequest { result ->
   // ネットワークリクエストが成功
   databaseSave(result) { rows ->
     // 結果の保存が完了
   }
}

Coroutinesを使うことでコールバックを使ったコードをシーケンシャルなコードに変換することができます。

// Coroutinesを使用した同じコード
val result = networkRequest()
// 成功したネットワーク要求
databaseSave(result)
//結果を保存しました

最初はアーキテクチャ コンポーネントを使用して構築された、既存のアプリがある状態から始めます。このアプリでは長時間実行タスクにコールバックを使用しています。

このコードラボが終わる頃には、既存のAPIをCoroutinesを使用したものへの変換や、Coroutinesのアプリへの組み込みができるようになっていることでしょう。また、Coroutinesのベストプラクティスと、Coroutinesを使用するコードに対するテストの書き方についても理解できていることでしょう。

このコードラボで学べること

必要となる前提知識

このコードラボで必要なもの

コードのダウンロード

以下のリンクからこのコードラボで必要なコードすべてをダウンロードすることができます。

Zipをダウンロード

または、以下のコマンドでコマンドラインからGitHubリポジトリをcloneすることもできます。

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

kotlin-coroutines リポジトリは3つの異なるapp projectsで構成されています:

よくある質問

まずはそのままの状態でサンプルアプリがどういう構造になっているかをみてみましょう。次の手順に従ってAndroid Studioでサンプルアプリを開きます。

  1. kotlin-coroutines zipファイルをダウンロードしている場合は、ファイルを展開します
  2. Android Studioでkotlin-coroutines-startプロジェクトを開きます
  3. [実行]ボタンをクリックし、エミュレータを選択するか、Androidデバイスを接続します。 Android Lollipopの実行(サポートされる最小SDKは21)。
  4. Runボタンをクリックし、エミュレータを選択するかAndroid端末を接続します。minimum SDKは21なので、Android端末はAndroid Lollipop以上である必要があります。Kotlin Coroutines画面が表示されるはずです。

このサンプルアプリは、画面のどこかをタップするとスレッドを使用して1秒後にSnackbarを表示します。実際に試してみると、少し間をおいて「Hello, from threads!」と表示されるはずです。このコードラボの最初の部分では、このアプリをCoroutinesを使用したものに置き換えます。

このアプリではアーキテクチャーコンポーネントを使ってMainActivity内のUI用コードとMainViewModelのアプリケーションロジックを分離させています。一度プロジェクトの構造をみてみましょう。

  1. MainActivityがUIの表示や、クリックリスナの登録、Snackbarの表示を行います。MainViewModelにイベントを渡し、MainViewModelLiveDataをもとに画面を更新します。
  2. MainViewModelonMainViewClickedでイベントをハンドリングし、LiveDataを使ってMainActivityに通達します。
  3. Executorsはバックグラウンドスレッドでコードを実行することができるBACKGROUNDを定義します。
  4. MainViewModelTestMainViewModelのテストを定義します。

プロジェクトにCoroutinesを追加する

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"
}

CoroutinesとRxJava

コードベースに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を実行し、準備ができたら結果を返すことが可能になります。

Coroutinesを使ってコールバックをなくす

コールバックは優れたパターンですが、いくつかの欠点があります。コールバックを頻繁に使用するコードは読みにくく、理解するのが難しくなります。またコールバックでは、例外などの一部の言語機能が使用できません。

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 { ... }
Coroutinesの別名

他の言語におけるasyncawaitパターンはCoroutinesに基づいて作られています。このパターンで例えると、suspendasyncと似ているといえます。ただしKotlinではsuspend関数を呼ぶと暗黙的にawait()相当の待機状態になります。

またKotlinには、asyncビルダーで生成されたCoroutinesの結果を待つのに使えるDeferred.await()メソッドもあります。

次はstart sample appをCoroutinesを使ったものに置き換えていきます。

この演習では、遅延後にメッセージを表示するcoroutineを作成します。はじめるにあたって、Android Studioでプロジェクトkotlin-coroutines-startを開いていることを確認してください。

MainViewModelにcoroutine scopeを追加

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)

この例では、uiScopeDispatchers.Main(Androidのメインスレッド)でcoroutineを実行します。Dispatchers.Mainで実行されるcoroutineは、中断中はメインスレッドをブロックしません。ViewModelで使うcoroutineはほとんどの場合メインスレッドでUIを更新するため、メインスレッドでcoroutineを開始するのが妥当でしょう。コードラボの後のほうでも出てきますが、main dispatcherで始まったcoroutineが別のdispatcherを使って大きなJSONをパースし結果をメインスレッドで扱うといったように、coroutineは開始後いつでもdispatcherを切り替えて実行することができます。

CoroutineContext


CoroutineScope
は、パラメーターとしてCoroutineContextを受け取ることができます。CoroutineContextはcoroutineを設定するためのattribute setです。スレッドポリシーやexception handlerなどを定義することができます。

上記の例では、CoroutineContextのプラス演算子を使ってスレッドポリシー(
Dispatchers.Main
)とjob(viewModelJob)を定義しています。演算結果のCoroutineContextは両方のcontextを合わせたものになります。

ViewModelがclearされたときにscopeをキャンセルする

ViewModelが使用されなくなり破棄されると、onClearedが呼び出されます。これは通常、ユーザーがViewModelを使用していたアクティビティやフラグメントから遷移したときに発生します。前の章で作成したscopeをキャンセルするには、次のコードを含める必要があります。

override fun onCleared() {
    super.onCleared()
    viewModelJob.cancel()
}

viewModelJobuiScopeのjobとして渡されるため、viewModelJobがキャンセルされると、uiScopeによって開始されたすべてのcoroutineもキャンセルされます。無駄ななタスクの実行やメモリリークを回避するために、不要になったcoroutineはキャンセルすることが重要です。

重要: scopeで開始されたすべてのcoroutineをキャンセルするには、 CoroutineScopeJobを渡す必要があります。そうしないとアプリが終了するまでscopeが実行されてしまい、意図しない挙動の場合メモリリークにつながってしまいます。

CoroutineScopeコンストラクタで作成されたスコープは暗黙的なjobを追加します。このjobはuiScope.coroutineContext.cancel()でキャンセルできます。

viewModelScopeを使用してボイラープレートを回避する

上記のコードはプロジェクト内すべての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()をオーバーライドする必要がない
}

スレッドからCoroutinesに切り替える

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を使ったコードに置き換えてみましょう。launchdelayを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を表示しますが、いくつか重要な違いがあります

  1. viewModelScope.launchviewModelScopeでcoroutineを開始します。これは、viewModelScopeに渡したジョブがキャンセルされると、このジョブまたはスコープ内のすべてのcoroutineがキャンセルされることを意味します。delayが返される前にユーザーがアクティビティを離れた場合、ViewModelを破棄する際にonClearedが呼び出されると、このcoroutineは自動的にキャンセルされます。
  2. viewModelScopeの既定のdispatcherはDispatchers.Mainなので、このcoroutineはメインスレッドでlaunchされます。他のスレッドを使う方法については後述されます。
  3. delaysuspend関数です。これは、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を提供します。

これにより以下が可能になります。

  1. delayの無効化
  2. 時間を明示的に制御して複数のcoroutineをテストする
  3. launchやasyncのcode blockを即時実行する
  4. テスト内のcoroutineの実行を一時停止、手動で進め、再開する
  5. 例外の発生をテスト失敗として報告する

詳細については、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つのことが起こります。

  1. ルールは、JUnitでテストを実行する前後にコードを実行する方法です。InstantTaskExecutorRuleは、テストの実行中にすぐにメインスレッドにpostするようにLiveDataを構成するJUnitルールです。
  2. 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を使う実装を変更してもテストは壊れることはありません。

既存のテストを実行する

  1. Android Studioでクラス名MainViewModelTestを右クリックして、コンテキストメニューを開きます。
  2. コンテキストメニューでRun MainViewModelTest を選択します。
  3. 2回目以降はツールバーのボタンの隣の一覧からこのtest configurationを選択できます。デフォルトでは、configurationはMainViewModelTestと命名されます。

前の章で実装したコードでテストを実行すると、assertion failureが発生します。

expected: Hello, from threads!
but was : Hello, from coroutines!

failしたテストを更新してpassさせる

"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に変換する方法について確認します。

delayはどうなった?

このテストにはまだ大きな問題が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のプロジェクトを開いてみましょう。

このアプリはアーキテクチャコンポーネントを使用して、ネットワークとローカルデータベースの両方を使用するデータレイヤーを、前章のプロジェクトに付け加える形で実装しています。メインビューがクリックされると、ネットワークから取得した新しいタイトルをデータベースに保存して、画面に表示します。少し時間をとって、新しいクラスに慣れましょう。

  1. MainDatabaseには、Roomを使用してTitleを読み書きするデータベースが実装されています。
  2. MainNetworkには、新しいタイトルを取得するネットワークAPIを実装します。FakeNetworkLibrary.ktで定義された偽のネットワークライブラリを使用してタイトルを取得します。ネットワークライブラリはランダムにエラーを返します。
    1。TitleRepositoryは、ネットワークとデータベースからのデータを組み合わせてタイトルを取得または更新するための単一のAPIを実装します。
    1。MainViewModelTestMainViewModelのテストを定義します。
    1。FakeNetworkCallAwaitTestは、このコードラボで後ほど完了するテストです。

コールバックを使った既存のAPI

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 -> {
                // エラー時の結果を処理
            }
        }
    }
}

コールバックを使った既存のAPIを中断関数に置き換える

refreshTitleFakeNetworkCallのコールバックを使用して実装されています。 この章では、refreshTitleをcoroutineとして書き換えられるようにネットワークAPIを中断関数として公開することを目指します。

Kotlinでは、suspendCoroutineを使ってコールバックを使ったAPIを中断関数に置き換えることができます。

suspendCoroutineを呼ぶことで現在のcoroutineがすぐに中断されます。suspendCoroutineを使うとcoroutineを再開するのに必要なcontinuationオブジェクトを取得することができます。 continuationは言葉通りcoroutineを「continue」(継続、再開)するのに必要なcontextを保持します。

suspendCoroutineが提供するcontinuationにはresumeresumeWithExceptionの2つの関数があります。いずれかの関数を呼び出すと、suspendCoroutineがすぐに再開されます。

suspendCoroutineを使用すると、コールバックを待つ前にcoroutineを中断させることができます。 その後コールバックが呼び出される際に、resumeresumeWithExceptionを呼ぶとコールバック結果を持って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へのキャンセルをサポートするライブラリにキャンセルをでんぱさせることが可能になります。

suspendCoroutineを使用して、コールバックを使ったAPIをCoroutinesに変換する

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
}

awaitsuspend関数であるため、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を使用してRepositoryViewModelでデータを取得する方法について取り上げます。

この章では、「TitleRepository」の実装を完了するために、Coroutinesが実行されるスレッドを切り替える方法を取り上げます。

refreshTitle内のコールバックを使った既存のコードを確認する

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.ktrefreshTitleメソッドには、呼び出し元に完了とエラーの状態を通知するコールバックが実装されています。2つのコールバックが連動するため、コードが少し読みづらくなっています。詳しく見てみましょう。

  1. リクエストが開始される前に、コールバックにリクエストがLoadingであることが通知される
  2. ネットワークの結果を待つのに、FakeNetworkCallに別のコールバックが登録される
  3. ネットワークから新しいタイトルが返されると、タイトルはバックグラウンドスレッドでデータベースに保存される
  4. 呼び出し元には、リクエストが完了したことが通知される(Loading状態でなくなる)
  5. リクエストが失敗した場合は、呼び出し元にはリクエストがエラーになったことが通知される(完了と同じで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.refreshTitleLoadingSuccessErrorのいずれかが繰り返し呼ばれるコールバックを渡します。受け取った状態をもとに、適切なLiveDataでUIが更新されます。

TitleRepositoryのコールバックを使ったコードをCoroutinesに置き換える

TitleRepository.ktを開いて、refreshTitleをCoroutinesを使った実装に置き換えます。置き換える実装ではRefreshStateTitleStateListenerは使用しないので、この段階で削除してしまいましょう。

// 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関数を使用してfetchNewWelcomesuspend関数に変換します。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の実装内で明示的にする必要がないことがわかります。

Kotlin APIでは中断関数を採用する

ここで定義されている拡張機能のawaitは、既存のAPIをcoroutineにブリッジさせるには良い方法です。しかし、呼び出し側がawaitを呼び出すことを忘れないことに依存します。Kotlinで使用するAPIを新しく設計するときは、suspend関数を通じて直接結果を返すほうが適切です。

suspend fun fetchNewWelcome: String

fetchNewWelcomeがこのような中断関数として再定義する場合、呼び出し元は使用するたびにawaitを呼び出すことを覚えておく必要はありません。

MainViewModelで中断関数を使用する

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
        }
    }
}

この実装では、通常のフロー制御を使用して例外をキャッチします。リポジトリのrefreshTItlesuspend関数であるため、発生した例外はtry/catchに公開されます。

スピナーを表示するロジックも簡単です。refreshTitleは更新が完了するまでcoroutineを中断させるため、コールバックを介してロード状態を明示的に渡す必要はありません。代わりに、スピナーは ViewModelによって制御され、finallyブロックで非表示になります。

キャッチされなかった例外はどうなるか

coroutineスコープのキャッチされない例外は、coroutineでない通常のコードのものに似ています。デフォルトではスコープに渡されたジョブはキャンセルされ、例外が uncaughtExceptionHandlerに渡されます。


CoroutineExceptionHandler
を使うことで、この挙動をカスタマイズすることができます。

appと選択してアプリを再度実行すると、画面をタップするとスピナーが表示されることが確認できます。タイトルはRoomデータベースから更新されます。エラーの場合は、スナックバーが表示されます。

次の章では、このコードを汎用的なデータ読み込み関数を使用するようにリファクタリングします。

この章では、MainViewModelrefreshTitleをリファクタリングして、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内に宣言することもできます。

高階関数でCoroutinesを使用する

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を使ったコードを使用する方法を取り上げます。

WorkManagerとは

Androidには、遅延可能なバックグラウンド作業を実行するための選択肢がいくつもあります。この章では、WorkManagerをCoroutinesで実装する方法を示します。WorkManagerは、遅延可能なバックグラウンド作業のための、互換性のある、柔軟でシンプルなライブラリです。WorkManagerは、Androidでこういったユースケースに対応する場合に推奨される手法です。

WorkManagerはAndroid Jetpackの一部であり、 機に便乗した実行(opportunistic execution)と保証される実行(guaranteed execution)の組み合わせが必要なバックグラウンド作業のためのアーキテクチャーコンポーネントです。機に便乗した実行とは、WorkManagerがバックグラウンドの作業をできるだけ早く行うことを意味します。保証される実行とは、ユーザーがアプリ外に遷移するなど、様々な状況下でWorkManagerが作業を開始するのに必要なロジックをこなすことを意味します。

このため、WorkManagerはいつか必ず完了する必要のあるタスクに適しています。

WorkManagerを適切に使用するタスクの例:

WorkManagerの詳細については、ドキュメントを参照ください。

WorkManagerでCoroutinesを使用する

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では実行されません。

CoroutineWorkerをテストする

テストがなければ完全なコードベースとはいえません。

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を参照ください。