Android アプリで Kotlin コルーチンを使用する

この Codelab では、Android アプリで Kotlin コルーチンを使用する方法を学習します。これは、コールバックの必要性を減らすことで、コードを簡素化できるバックグラウンド スレッドを管理する新しい方法です。コルーチンは、データベースやネットワーク アクセスなどの長時間実行タスクの非同期コールバックを順次コードに変換する Kotlin 機能です。

演習内容の抜粋となるコード スニペットをご覧ください。

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

コールバック ベースのコードは、コルーチンを使用して順次コードに変換されます。

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

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

この Codelab を終了すると、アプリでコルーチンを使用してネットワークからデータを読み込むことができ、コルーチンをアプリに統合できるようになります。コルーチンのベスト プラクティスと、コルーチンを使用するコードに対するテストの作成方法についても学習します。

前提条件

  • アーキテクチャ コンポーネントの ViewModelLiveDataRepositoryRoom に精通していること
  • 拡張関数やラムダを含む Kotlin 構文の使用経験
  • メインスレッド、バックグラウンド スレッド、コールバックなど、Android でのスレッドの使用に関する基本的な知識

演習内容

  • コルーチンを使用して記述されたコードを呼び出し、結果を取得する。
  • suspend 関数を使用して非同期コードを順次処理します。
  • launchrunBlocking を使用して、コードの実行を制御します。
  • suspendCoroutine を使用して既存の API をコルーチンに変換するテクニックを学びます。
  • アーキテクチャ コンポーネントでコルーチンを使用する。
  • コルーチンをテストするためのおすすめの方法について学びます。

必要なもの

  • Android Studio 3.5(この Codelab は他のバージョンでも動作する可能性がありますが、一部が欠けたり、外観が異なったりすることがあります)。

この Codelab で問題(コードのバグ、文法的な誤り、不明確な表現など)が見つかった場合は、Codelab の左下隅にある [誤りを報告] から問題を報告してください。

コードをダウンロードする

次のリンクをクリックして、この Codelab 用のコードすべてをダウンロードします。

ZIP をダウンロード

または次のコマンドを使用して、コマンドラインから GitHub リポジトリのクローンを作成します。

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

よくある質問

まず、サンプルアプリがどんなものか見てみましょう。次の手順に沿って、Android Studio でサンプルアプリを開いてください。

  1. kotlin-coroutines zip ファイルをダウンロードした場合は、ファイルを解凍します。
  2. Android Studio で coroutines-codelab プロジェクトを開きます。
  3. start アプリケーション モジュールを選択します。
  4. execute.png実行)ボタンをクリックして、エミュレータを選択するか、Android デバイスを接続します。Android Lollipop は SDK が 21 以上搭載されている必要があります。Kotlin コルーチン画面が表示されます。

このスターター アプリは、画面をタップした後、スレッドを使用してカウントを少しずつ増やします。また、ネットワークから新しいタイトルを取得し、画面に表示します。試してみると、しばらくするとカウントとメッセージの表示が変わります。この Codelab では、このアプリケーションをコルーチンを使用するように変換します。

このアプリは、アーキテクチャ コンポーネントを使用して、MainActivity の UI コードを MainViewModel のアプリケーション ロジックから分離します。このプロジェクトの構造をよく理解しておいてください。

  1. MainActivity で UI を表示し、クリック リスナーを登録して、Snackbar を表示できます。イベントを MainViewModel に渡し、MainViewModelLiveData に基づいて画面を更新します。
  2. MainViewModel は、onMainViewClicked 内のイベントを処理し、LiveData. を使用して MainActivity と通信します。
  3. Executors は、バックグラウンド スレッドで実行できる BACKGROUND, を定義します。
  4. TitleRepository は、ネットワークからの結果を取得してデータベースに保存します。

プロジェクトにコルーチンを追加する

Kotlin でコルーチンを使用するには、プロジェクトの build.gradle (Module: app) ファイルに coroutines-core ライブラリを含める必要があります。Codelab プロジェクトにより、この操作はすでに完了しているため、Codelab を完了するためにこの操作を行う必要はありません。

Android のコルーチンは、コアライブラリと、Android 固有の拡張機能として使用できます。

  • kotlinx-corountines-core - Kotlin でコルーチンを使用するためのメイン インターフェース
  • kotlinx-coroutines-android - コルーチンにおける Android メインスレッドのサポート。

スターター アプリには、すでに build.gradle. の依存関係が含まれています。新しいアプリ プロジェクトを作成するときは、build.gradle (Module: app) を開いて、コルーチンの依存関係をプロジェクトに追加する必要があります。

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Android では、メインスレッドをブロックしないようにすることが不可欠です。メインスレッドは、UI の更新をすべて処理する単一のスレッドです。また、すべてのクリック ハンドラと他の UI コールバックを呼び出すスレッドでもあります。そのため、優れたユーザー エクスペリエンスを保証するには、スムーズに実行する必要があります。

ユーザーが視認できるような途切れがなくアプリを表示させるには、メインスレッドが 16 ミリ秒以上(約 60 フレーム/秒)の画面を更新する必要があります。大規模な JSON データセットの解析、データベースへのデータの書き込み、ネットワークからのデータ取得など、多くの一般的なタスクにはこれよりも時間がかかります。そのため、メインスレッドからこのようなコードを呼び出すと、アプリが一時停止したり、途切れたり、フリーズしたりする可能性があります。メインスレッドを長時間ブロックすると、アプリがクラッシュして [Application Not Responding] ダイアログが表示される場合があります。

以下の動画では、コルーチンによってメインセーフティを導入することで Android でこの問題を解決する方法を紹介しています。

コールバック パターン

メインスレッドをブロックせずに長時間実行タスクを実行するためのパターンの 1 つがコールバックです。コールバックを使用すると、長時間実行されるタスクをバックグラウンド スレッドで開始できます。タスクが完了すると、コールバックが呼び出され、メインスレッドで結果が通知されます。

コールバック パターンの例を見てみましょう。

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

このコードには @UiThread アノテーションが付いているため、メインスレッドで実行するのに十分な速度で実行される必要があります。つまり、次の画面更新が遅れないように、すぐに戻る必要があります。一方、slowFetch が完了するまでに数秒から数分かかるため、メインスレッドは結果を待ちます。show(result) コールバックを使用すると、slowFetch をバックグラウンド スレッドで実行し、準備ができたときに結果を返すことができます。

コルーチンを使用してコールバックを削除する

コールバックは優れたパターンですが、デメリットもあります。コールバックを多用するコードは読み取りと推測が困難になる可能性があります。また、コールバックでは一部の言語機能(例外など)を使用できません。

Kotlin コルーチンを使用すると、コールバック ベースのコードを順次コードに変換できます。一般的に、シーケンシャルに記述されたコードは読みやすく、例外などの言語機能を使用する場合もあります。

最終的に、長時間実行タスクから結果が得られるのを待ってから実行を継続します。ただし、コードでは、動作が異なります。

キーワード suspend は、コルーチンで使用できる関数または関数の型を Kotlin でマークを付ける方法です。コルーチンが suspend とマークされた関数を呼び出すと、その関数は通常の関数呼び出しのように返されるまでブロックするのではなく、結果の準備が整うまで実行を中断し、結果から中断した位置で再開します。結果の待機中に停止している間は、実行中のスレッドのブロックを解除して、他の関数またはコルーチンを実行できるようにします。

たとえば、次のコードでは、makeNetworkRequest()slowFetch() はどちらも suspend 関数です。

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

コールバック バージョンと同様に、makeNetworkRequest@UiThread とマークされているため、すぐにメインスレッドから戻る必要があります。これは通常、slowFetch のようなブロック メソッドを呼び出すことができなかったことを意味します。ここでは、suspend キーワードを使用します。

コールバック ベースのコードと比較して、コルーチンのコードは少ないコードで現在のスレッドのブロックを解除できます。順次スタイルにより、複数のコールバックを作成することなく、複数の長時間実行タスクを簡単に連結できます。たとえば、2 つのネットワーク エンドポイントから結果をフェッチしてデータベースに保存するコードは、コールバックなしのコルーチンの関数として記述できます。たとえば、次のように指定します。

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

次のセクションでは、サンプルアプリにコルーチンを紹介します。

この演習では、遅延後にメッセージを表示するコルーチンを作成します。まず、Android Studio でモジュール start が開いていることを確認します。

CoroutineScope について

Kotlin では、すべてのコルーチンが CoroutineScope 内で実行されます。スコープは、そのジョブを通じてコルーチンの存続期間を制御します。スコープのジョブをキャンセルすると、そのスコープで開始されたコルーチンがすべてキャンセルされます。Android では、スコープを使用して、たとえば Activity または Fragment から離れたときに、実行中のすべてのコルーチンをキャンセルできます。スコープでは、デフォルトのディスパッチャを指定することもできます。ディスパッチャは、ディスパッチャを制御するスレッドを制御します。

UI によって開始されたコルーチンの場合、通常は Android のメインスレッドである Dispatchers.Main で開始することをおすすめします。Dispatchers.Main で開始されたコルーチンは、停止している間にメインスレッドをブロックしません。ViewModel コルーチンはほとんどの場合メインスレッドで UI を更新するため、メインスレッドでコルーチンを開始すると、余分なスレッド切り替えを節約できます。メインスレッドで開始されたコルーチンは、開始後にいつでもディスパッチャを切り替えることができます。たとえば、別のディスパッチャを使用して、メインスレッド外の大きな JSON 結果を解析できます。

viewModelScope の使用

AndroidX lifecycle-viewmodel-ktx ライブラリは、UI 関連のコルーチンを開始するように構成された ViewModel に CoroutineScope を追加します。このライブラリを使用するには、プロジェクトの build.gradle (Module: start) ファイルに追加する必要があります。その手順は Codelab プロジェクトですでに完了しています。

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

ライブラリは、viewModelScopeViewModel クラスの拡張関数として追加します。このスコープは Dispatchers.Main にバインドされ、ViewModel が消去されると自動的にキャンセルされます。

スレッドからコルーチンに切り替える

MainViewModel.kt で、次のコードと合わせて次の TODO を探します。

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

このコードは、BACKGROUND ExecutorServiceutil/Executor.kt で定義)を使用してバックグラウンド スレッドで実行します。sleep は現在のスレッドをブロックするため、メインスレッドで呼び出された場合は UI をフリーズします。ユーザーがメインビューをクリックして 1 秒後に、スナックバーをリクエストします。

この処理は、コードから BACKGROUND を削除して、もう一度実行することにより確認できます。読み込みスピナーが表示されず、すべてが 1 秒後に最終状態に遷移します。

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

updateTaps を、同じ処理を行うコルーチン ベースのコードに置き換えます。launchdelay をインポートする必要があります。

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

このコードは同じ処理を行い、スナックバーを表示するまで 1 秒待機します。ただし、次のような重要な違いがあります。

  1. viewModelScope.launchviewModelScope でコルーチンを開始します。つまり、viewModelScope に渡されたジョブがキャンセルされると、このジョブ/スコープ内のすべてのコルーチンがキャンセルされます。delay が返される前にユーザーがアクティビティを終了した場合、ViewModel の破棄で onCleared が呼び出されると、このコルーチンは自動的にキャンセルされます。
  2. viewModelScope には Dispatchers.Main のデフォルト ディスパッチャがあるため、このコルーチンはメインスレッドで起動されます。別のスレッドの使用方法については、後で説明します。
  3. 関数 delaysuspend 関数です。Android Studio では、左のガターの アイコンで示されます。このコルーチンはメインスレッドで実行されますが、delay はスレッドを 1 秒間ブロックしません。代わりに、ディスパッチャは、次のステートメントで 1 秒以内にコルーチンを再開するようにスケジュール設定します。

実行してみましょう。メインビューをクリックすると、1 秒後にスナックバーが表示されます。

次のセクションでは、この機能のテスト方法について説明します。

この演習では、先ほど記述したコードのテストを作成します。この演習では、kotlinx-coroutines-test ライブラリを使用して、Dispatchers.Main で実行されているコルーチンをテストする方法を示します。この Codelab の後半では、コルーチンを直接操作するテストを実装します。

既存のコードを確認する

androidTest フォルダで MainViewModelTest.kt を開きます。

MainViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

ルールとは、JUnit でテストを実行する前後にコードを実行する方法です。デバイス外テストで MainViewModel をテストするには、次の 2 つのルールを使用します。

  1. InstantTaskExecutorRule は、各タスクを同期的に実行するように LiveData を設定する JUnit ルールです。
  2. MainCoroutineScopeRule は、kotlinx-coroutines-testTestCoroutineDispatcher を使用するように Dispatchers.Main を構成する、このコードベースのカスタムルールです。これにより、テストで仮想時計をテストしたり、単体テストでコードで Dispatchers.Main を使用したりできるようになります。

setup メソッドで、MainViewModel の新しいインスタンスをテスト用の疑似を使用して作成します。これらは、実際のネットワークやデータベースを使用せずにテストを作成できるように、スターター コードで提供されるネットワークとデータベースの疑似実装です。

このテストでは、疑似メソッドは MainViewModel の依存関係を満たす場合にのみ必要です。この Codelab の後半では、コルーチンをサポートするようにフェイクを更新します。

コルーチンを制御するテストを作成する

メインビューがクリックされた後に 1 秒以内にタップが更新されることを確認する新しいテストを追加します。

MainViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

onMainViewClicked を呼び出すと、作成したコルーチンが開始されます。このテストでは、onMainViewClicked が呼び出された直後にタップ テキストが「0 タップ」に維持され、1 秒後に「1 タップ」に更新されます。

このテストでは、仮想時間を使用して、onMainViewClicked によって開始されたコルーチンの実行を制御します。MainCoroutineScopeRule を使用すると、Dispatchers.Main で開始されたコルーチンの実行を一時停止、再開、制御できます。ここでは、advanceTimeBy(1_000) を呼び出しています。これにより、メイン ディスパッチャは 1 秒後に再開するようにスケジュール設定されたコルーチンをすぐに実行します。

このテストは完全に確定的であるため、常に同じ方法で実行されます。また、Dispatchers.Main で起動されたコルーチンの実行を完全に制御できるため、値が設定されるまで 1 秒待つ必要はありません。

既存のテストを実行する

  1. エディタでクラス名 MainViewModelTest を右クリックして、コンテキスト メニューを開きます。
  2. コンテキスト メニューで、[execute.pngRun 'MainViewModelTest'] を選択します。
  3. 今後の実行では、ツールバーの execute.png ボタンの横にある設定でこのテスト構成を選択できます。デフォルトでは、この構成の名前は MainViewModelTest になります。

テストに合格するはずです。実行時間は 1 秒より少し短いです。

次の演習では、コルーチンを使用するように既存のコールバック API から変換する方法を学習します。

このステップでは、コルーチンを使用するようにリポジトリの変換を開始します。これを行うために、ViewModelRepositoryRoomRetrofit にコルーチンを追加します。

コルーチンを使用するように切り替える前に、アーキテクチャの各部分の影響を把握しておくことをおすすめします。

  1. MainDatabase は、Title を保存して読み込む Room を使用してデータベースを実装します。
  2. MainNetwork は、新しいタイトルをフェッチするネットワーク API を実装します。Retrofit を使用して、タイトルを取得します。Retrofit は、エラーまたはモックデータをランダムに返すように設定されますが、実際のネットワーク リクエストを行っているかのように動作します。
  3. TitleRepository は、ネットワークとデータベースからのデータを結合してタイトルを取得または更新するための単一の API を実装します。
  4. MainViewModel は画面の状態を表し、イベントを処理します。この操作により、ユーザーが画面をタップしたときにタイトルを更新するようリポジトリに指示されます。

ネットワーク リクエストは UI イベントによって駆動され、それらに基づいてコルーチンを開始したいので、コルーチンを使い始めるのは通常 ViewModel にあります。

コールバック バージョン

MainViewModel.kt を開き、refreshTitle の宣言を確認します。

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

この関数は、ユーザーが画面をクリックするたびに呼び出されます。これにより、リポジトリのタイトルが更新され、新しいタイトルがデータベースに書き込まれます。

この実装では、次の処理を行うためにコールバックを使用します。

  • クエリが開始されると、読み込みスピナーと _spinner.value = true が表示されます。
  • 結果が生成されると、_spinner.value = false で読み込みスピナーがクリアされます。
  • エラーが発生すると、スナックバーにスピナーが表示され、クリアされます。

なお、onCompleted コールバックには title は渡されません。すべてのタイトルを Room データベースに書き込むため、UI は、Room によって更新された LiveData を監視して現在のタイトルに更新します。

コルーチンの更新では、まったく同じ動作が維持されます。UI データベースを自動的に最新の状態に保つために、Room データベースのような監視可能なデータソースを使用するのが良いパターンです。

コルーチンのバージョン

コルーチンを使用して refreshTitle を書き換えましょう。

すぐに必要になるため、リポジトリで空の suspend 関数(TitleRespository.kt)を作成します。suspend 演算子を使用してコルーチンで動作することを Kotlin に伝える新しい関数を定義します。

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

この Codelab が完了したら、Retrofit と Room を使用して、コルーチンを使用して新しいタイトルを取得し、データベースに書き込むように更新します。当面は、500 ミリ秒を装って仕事をし、続行します。

MainViewModel で、refreshTitle のコールバック バージョンを新しいコルーチンを開始するバージョンに置き換えます。

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

この関数の手順を見ていきましょう。

viewModelScope.launch {

コルーチンを設定してタップ回数を更新するのと同様に、viewModelScope で新しいコルーチンを開始します。これは Dispatchers.Main を使用しますが、問題はありません。refreshTitle はネットワーク リクエストとデータベース クエリを行いますが、コルーチンを使用してメインセーフ インターフェースを公開できます。つまり、メインスレッドから安全に呼び出せます。

viewModelScope を使用しているため、ユーザーがこの画面から離れると、このコルーチンで開始された処理は自動的にキャンセルされます。つまり、追加のネットワーク リクエストやデータベース クエリは行われません。

次のコード行は、実際には repositoryrefreshTitle を呼び出します。

try {
    _spinner.value = true
    repository.refreshTitle()
}

このコルーチンは、読み込みスピナーが開始される前に、通常の関数と同じように refreshTitle を呼び出します。ただし、refreshTitle は suspend 関数であるため、通常の関数とは動作が異なります。

コールバックを渡す必要はありません。コルーチンは、refreshTitle で再開されるまで停止します。これは通常のブロッキング関数呼び出しに似ていますが、ネットワークとデータベースへのクエリが完了するまで待機してから、メインスレッドをブロックせずに再開します。

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

suspend 関数の例外は、通常の関数のエラーと同じように動作します。suspend 関数でエラーをスローすると、呼び出し元にスローされます。そのため、実行方法がかなり異なっていても、通常の try/catch ブロックを使用して処理できます。これは、すべてのコールバックのカスタムエラー処理を構築する代わりに、エラー処理に組み込みの言語サポートを使用できるため便利です。

また、コルーチンから例外をスローすると、そのコルーチンはデフォルトで親をキャンセルします。つまり、関連する複数のタスクをまとめて簡単にキャンセルできます。

そして、最後のブロックでは、クエリの実行後にスピナーが常にオフになっていることを確認できます。

[start] 設定を選択し、execute.png を押してアプリを再び実行すると、任意の場所をタップすると読み込み中のスピナーが表示されます。ネットワークやデータベースがまだ接続されていないため、タイトルは変わりません。

次の演習では、実際に動作するようにリポジトリを更新します。

この演習では、TitleRepository の作業バージョンを実装するために、コルーチンを実行するスレッドを切り替える方法を学びます。

refreshTitle で既存のコールバック コードを確認する

TitleRepository.kt を開き、既存のコールバック ベースの実装を確認します。

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

TitleRepository.ktrefreshTitleWithCallbacks メソッドがコールバックとともに実装され、読み込み状態とエラー状態を呼び出し元に知らせます。

この関数は、更新機能を実装するために多くの処理を行います。

  1. BACKGROUND ExecutorService で別のスレッドに切り替える
  2. ブロック execute() メソッドを使用して fetchNextTitle ネットワーク リクエストを実行します。これにより、現在のスレッド(この場合は BACKGROUND のスレッド)でネットワーク リクエストが実行されます。
  3. 結果が成功の場合、insertTitle でデータベースに保存し、onCompleted() メソッドを呼び出します。
  4. 結果が失敗した場合や例外がある場合は、onError メソッドを呼び出して、更新の失敗について呼び出し元に知らせます。

このコールバック ベースの実装はメインスレッドをブロックしないため、メインセーフです。しかし、コールバックを使用して、処理が完了したときに呼び出し元に通知する必要があります。また、切り替えた BACKGROUND スレッドのコールバックも呼び出します。

コルーチンからのブロック呼び出しの呼び出し

ネットワークまたはデータベースにコルーチンを導入しなくても、コルーチンを使用してこのコードをメインセーフにすることができます。これにより、コールバックを取り除き、最初に呼び出したスレッドに結果を返すことができます。

このパターンは、大規模なリストの並べ替えやフィルタリング、ディスクからの読み取りなど、コルーチン内からブロッキングまたは CPU を大量に消費する作業を行う必要がある場合に使用できます。

ディスパッチャを切り替えるために、コルーチンは withContext を使用します。withContext を呼び出すと、ラムダのためだけに他のディスパッチャに戻ります。その後、そのラムダの結果で呼び出したディスパッチャに戻ります。

デフォルトでは、Kotlin コルーチンには MainIODefault の 3 つのディスパッチャが用意されています。IO ディスパッチャはネットワークやディスクからの読み取りなどの IO 作業向けに、またデフォルトのディスパッチャは CPU 使用率の高いタスク向けに最適化されています。

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

この実装では、ネットワークとデータベースに対してブロッキング呼び出しを使用しますが、コールバック バージョンよりはややシンプルです。

このコードでは引き続きブロック呼び出しが使用されます。execute()insertTitle(...) を呼び出すと、このコルーチンが実行されているスレッドがブロックされます。ただし、withContext を使用して Dispatchers.IO に切り替えると、IO ディスパッチャの 1 つのスレッドがブロックされます。この呼び出し元のコルーチン(場合によっては Dispatchers.Main で実行されています)は、withContext ラムダが完了するまで停止します。

コールバック バージョンと比較すると、次の 2 つの重要な違いがあります。

  1. withContext はそれを呼び出したディスパッチャに結果を返します(この場合は Dispatchers.Main)。コールバック バージョンは、BACKGROUND エグゼキュータ サービスのスレッドでコールバックを呼び出します。
  2. 呼び出し元は、この関数にコールバックを渡す必要はありません。一時停止または再開して結果やエラーを返すことができます。

アプリを再度実行する

アプリを再度実行すると、新しいコルーチン ベースの実装がネットワークから結果を読み込んでいます。

次のステップでは、コルーチンを Room と Retrofit に統合します。

コルーチンの統合を続けるために、Room と Retrofit の安定バージョンで suspend 関数のサポートを使用し、suspend 関数を使用して記述したコードを大幅に簡素化します。

Room 内のコルーチン

まず、MainDatabase.kt を開き、insertTitle を suspend 関数にします。

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

これを行うと、Room はクエリをメインセーフにして、バックグラウンド スレッドで自動的に実行します。ただし、このクエリはコルーチン内からしか呼び出せません。

そして、Room でコルーチンを使用する手順はこれで完了です。非常に良い。

Retrofit のコルーチン

次は、コルーチンを Retrofit と統合する方法を見てみましょう。MainNetwork.kt を開き、fetchNextTitle を suspend 関数に変更します。

MainNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Retrofit で suspend 関数を使用するには、次の 2 つの作業を行う必要があります。

  1. 関数に suspend 修飾子を追加する
  2. 戻り値の型から Call ラッパーを削除します。ここでは String が返されますが、複雑な json 型型も返すことができます。引き続きレトロフィットの完全な Result へのアクセス権を付与する必要がある場合は、suspend 関数から String ではなく Result<String> を返すことができます。

Retrofit は suspend 関数を自動的にメインセーフにするので、Dispatchers.Main から直接呼び出すことができます。

Room と Retrofit の使用

Room と Retrofit が suspend 関数をサポートするようになったので、それらをリポジトリから使用できます。TitleRepository.kt を開き、suspend 関数を使用するとブロッキング バージョンと比べてロジックが大幅に簡素化されることを確認します。

タイトル Repository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

かなり短いです。どうなりましたか?一時停止と再開に依存することで、コードがかなり短縮されることがわかりました。Retrofit では、Call ではなく、StringUser オブジェクトなどの戻り値の型をここで使用できます。suspend 関数内で、Retrofit はバックグラウンド スレッドでネットワーク リクエストを実行し、呼び出しが完了するとコルーチンを再開できるため、これは安全です。

さらに良いことに、withContext を廃止しました。Room と Retrofit はどちらもメインセーフの suspend 関数を提供するため、Dispatchers.Main からこの非同期処理をオーケストレートしても安全です。

コンパイラ エラーの修正

コルーチンに移行するには、関数のシグネチャを変更しなければなりません。通常の関数から suspend 関数を呼び出すことができないためです。このステップで suspend 修飾子を追加すると、実際のプロジェクトで一時停止するように関数を変更した場合に発生する処理を示す、コンパイラ エラーがいくつか生成されます。

プロジェクトを調べ、関数を作成した suspend に変更してコンパイラ エラーを修正します。主な解決策は次のとおりです。

TestingFakes.kt

テスト用の suspend 修飾子をサポートするように更新してください。

titleDaoFake

  1. Alt+Enter キーを押し、階層内のすべての関数に一時停止修飾子を追加する

MainNetworkFake

  1. Alt+Enter キーを押し、階層内のすべての関数に一時停止修飾子を追加する
  2. fetchNextTitle をこの関数に置き換えます。
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Alt+Enter キーを押し、階層内のすべての関数に一時停止修飾子を追加する
  2. fetchNextTitle をこの関数に置き換えます。
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • refreshTitleWithCallbacks 関数は使用されなくなったため、削除します。

アプリを実行する

アプリを再度実行すると、コンパイル後に、ViewModel から Room と Retrofit まで、コルーチンを使用してデータを読み込み中であることがわかります。

これで、このアプリはコルーチンを使用するように完全に入れ替えられました。最後に、先ほど行ったテスト方法について説明します。

この演習では、suspend 関数を直接呼び出すテストを作成します。

refreshTitle は公開 API として公開されているため、直接テストされ、テストからコルーチン関数を呼び出す方法を示しています。

前の演習で実装した refreshTitle 関数を次に示します。

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

suspend 関数を呼び出すテストを作成する

2 つの TODO を含む test フォルダで TitleRepositoryTest.kt を開きます。

最初のテスト whenRefreshTitleSuccess_insertsRows から refreshTitle を呼び出します。

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

refreshTitlesuspend 関数であるため、Kotlin ではコルーチンや他の suspend 関数以外からの呼び出し方法については認識されません。また、次のようなコンパイラ エラーが発生します。suspend 関数 refreshTitle はコルーチンまたは別の suspend 関数からのみ呼び出す必要があります

テストランナーはコルーチンについて何も認識していないため、このテストを suspend 関数にすることはできません。ViewModel のように CoroutineScope を使用してコルーチンを launch することもできますが、テストでは、戻る前にコルーチンを完了する必要があります。テスト関数が返されたら、テストは終了です。launch で始まるコルーチンは非同期コードであり、将来的には完了する可能性があります。そのため、非同期コードをテストするには、コルーチンが完了するまで待機するようテストに伝える方法が必要です。launch は非ブロッキング呼び出しであるため、すぐに返され、関数が返された後も引き続きコルーチンを実行できます。この関数をテストでは使用できません。次に例を示します。

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

このテストは一部が失敗することがあります。launch の呼び出しはすぐに返され、他のテストケースと同時に実行されます。このテストでは、refreshTitle が実行されているかどうかを知る方法がありません。データベースが更新されたことを確認するなどのアサーションは不安定です。また、refreshTitle が例外をスローした場合、テスト コールスタックでスローされません。代わりに、捕捉されなかった例外のハンドラに GlobalScope がスローされます。

ライブラリ kotlinx-coroutines-test には、suspend 関数を呼び出している間にブロックを行う runBlockingTest 関数があります。runBlockingTest は、suspend 関数を呼び出すか、新しいコルーチンを launches すると、デフォルトですぐに実行します。これは、suspend 関数とコルーチンを通常の関数呼び出しに変換する方法と考えることができます。

また、runBlockingTest は、キャッチされない例外をスローします。これにより、コルーチンが例外をスローしたときのテストが容易になります。

1 つのコルーチンでテストを実装する

refreshTitle の呼び出しを runBlockingTest でラップし、subject.refreshTitle() から GlobalScope.launch ラッパーを削除します。

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

このテストでは、提供されている仮のものを使用して、refreshTitle によって「OK」がデータベースに挿入されていることを確認します。

テストが runBlockingTest を呼び出すと、runBlockingTest によって開始されたコルーチンが完了するまでブロックされます。その後、refreshTitle を呼び出し、通常の一時停止および再開メカニズムを使用して、データベース行が疑似に追加されるのを待ちます。

テストコルーチンが完了すると、runBlockingTest が返されます。

タイムアウト テストを作成する

ネットワーク リクエストに短いタイムアウトを追加します。最初にテストを作成してから、タイムアウトを実装します。新しいテストを作成します。

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

このテストでは、提供された偽の MainNetworkCompletableFake を使用します。これは、テストが続行されるまで発信者を一時停止するように設計されたネットワーク フェイクです。refreshTitle がネットワーク リクエストを行うと、タイムアウトをテストするため、完全に停止します。

次に、別のコルーチンを起動して refreshTitle を呼び出します。これは、タイムアウトをテストする際の重要な要素です。runBlockingTest が作成するものとは異なるコルーチンでタイムアウトを発生させる必要があります。次の行 advanceTimeBy(5_000) を呼び出して、時間を 5 秒進めて他のコルーチンをタイムアウトさせることができます。

これは完全なタイムアウト テストであり、タイムアウトを実装すると合格します。

今すぐ実行して動作を確認します。

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

runBlockingTest の特長の 1 つは、テストの完了後にコルーチンをリークしないことです。テストの最後に終了したコルーチン(起動コルーチンなど)がある場合、テストは失敗します。

タイムアウトを追加する

TitleRepository を開き、ネットワーク取得に 5 秒のタイムアウトを追加します。これを行うには、withTimeout 関数を使用します。

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

テストを実行します。テストを実行すると、すべてのテストに合格するはずです。

次の演習では、コルーチンを使用して高階関数を記述する方法を学習します。

この演習では、一般的なデータ読み込み関数を使用するように、MainViewModelrefreshTitle をリファクタリングします。ここでは、コルーチンを使用する高階関数の作成方法について説明します。

refreshTitle の現在の実装は機能しますが、スピナーを常に表示する一般的なデータ読み込みコルーチンを作成できます。これは、複数のイベントに応答してデータを読み込むコードベースで、読み込みスピナーが一貫して表示されるようにする場合に便利です。

repository.refreshTitle() を除くすべての行で現在の実装を確認すると、スピナーと表示のエラーを示すボイラープレートが表示されます。

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

高階関数でのコルーチンの使用

次のコードを MainViewModel.kt に追加します

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

次に、refreshTitle() をリファクタリングして、この高階関数を使用します。

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

読み込みスピナーの表示とエラーの表示に関するロジックを抽象化することで、データの読み込みに必要な実際のコードを簡略化しました。スピナーの表示やエラーの表示は、あらゆるデータの読み込みに一般化されやすいものの、実際のデータソースと宛先は毎回指定する必要があります。

この抽象化を作成するために、launchDataLoad は一時停止のラムダである引数 block を受け取ります。サスペンド ラムダを使用すると、suspend 関数を呼び出すことができます。Kotlin は、この Codelab で使用していたコルーチン ビルダー launchrunBlocking を実装します。

// suspend lambda

block: suspend () -> Unit

一時停止ラムダを作成するには、suspend キーワードで始めます。関数の矢印と戻り値の型 Unit により、宣言が完了します。

たいていは独自のサスペンド ラムダを宣言する必要はありませんが、繰り返しロジックをカプセル化するこのような抽象化を作成するのに役立ちます。

この演習では、WorkManager のコルーチン ベースのコードを使用する方法を学びます。

WorkManager とは

Android には、遅延可能なバックグラウンド処理を行うためのさまざまな方法が用意されています。この演習では、WorkManager をコルーチンと統合する方法について説明します。WorkManager は、遅延可能なバックグラウンド処理用のライブラリで、互換性、柔軟性、シンプルさに優れています。WorkManager は、こうしたユースケースで Android に対して推奨されるソリューションです。

WorkManager は Android Jetpack の一部であり、待機的実行と確実な実行というニーズの組み合わせをもつバックグラウンド処理のためのアーキテクチャ コンポーネントです。待機的実行とは、WorkManager がバックグラウンド処理を可能になり次第実行することを指します。確実な実行とは、たとえばアプリを終了した場合など、さまざまな状況下で WorkManager がその処理の開始ロジックを保持して実行することを指します。

そのため、最終的に完了する必要があるタスクには WorkManager が適しています。

WorkManager の使用が適したタスクの例を以下に示します。

  • ログのアップロード
  • 画像へのフィルタ適用と画像の保存
  • ローカルデータとネットワークとの定期的な同期

WorkManager でコルーチンを使用する

WorkManager は、ユースケースごとに異なる基本 ListanableWorker クラスの実装を提供しています。

最もシンプルな Worker クラスを使用すると、WorkManager でなんらかの同期オペレーションを実行できます。ただし、これまでのところコルーチンと suspend 関数を使用するようにコードベースを変換してきたので、WorkManager の最良の方法は、doWork() 関数を suspend 関数として定義できる CoroutineWorker クラスを介して行うことです。

まず、RefreshMainDataWork を開きます。これはすでに CoroutineWorker を拡張しているため、doWork を実装する必要があります。

suspend doWork 関数内で、リポジトリから refreshTitle() を呼び出し、適切な結果を返します。

TODO を完了すると、コードは次のようになります。

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

CoroutineWorker.doWork() は suspend 関数です。単純な Worker クラスとは異なり、このコードは WorkManager の構成で指定されたエグゼキュータでは実行されず、代わりに coroutineContext メンバー(デフォルトでは Dispatchers.Default)のディスパッチャを使用します。

CoroutineWorker をテストする

コードベースはテストを行わないと完成しません。

WorkManager には、Worker クラスをテストするいくつかの方法が用意されています。元のテスト インフラストラクチャについて詳しくは、ドキュメントをご覧ください。

WorkManager v2.1 では、ListenableWorker クラスと CoroutineWorker を簡単にテストするための新しい API セットが導入されました。ここでは、新しい API TestListenableWorkerBuilder を使用します。

新しいテストを追加するには、androidTest フォルダにある RefreshMainDataWorkTest ファイルを更新します。

ファイルの内容は次のとおりです。

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

テストを開始する前に、ファクトリーについて WorkManager に伝え、疑似ネットワークを注入できるようにします。

テスト自体は、TestListenableWorkerBuilder を使用してワーカーを作成し、startWork() メソッドを呼び出します。

WorkManager は、コルーチンを使用して API の設計を簡素化する方法の一例にすぎません。

この Codelab では、アプリでコルーチンを使い始めるために必要な基本事項について学習しました。

内容は以下のとおりです。

  • UI ジョブと WorkManager ジョブの両方からコルーチンを Android アプリに統合し、非同期プログラミングを簡素化する方法
  • ViewModel 内のコルーチンを使用してネットワークからデータを取得し、メインスレッドをブロックすることなくデータベースに保存する方法。
  • ViewModel の終了時にすべてのコルーチンをキャンセルします。

コルーチン ベースのコードをテストする場合、動作をテストする方法と、テストから suspend 関数を直接呼び出す方法を説明しました。

詳細

Android での高度なコルーチン使用法については、「Kotlin Flow と LiveData を使用した高度なコルーチン」の Codelab をご覧ください。

Kotlin のコルーチンには、この Codelab では取り上げていない機能が多数あります。Kotlin のコルーチンについて詳しくは、JetBrains が公開しているコルーチン ガイドをご覧ください。Android のコルーチンの使用パターンについて詳しくは、Kotlin コルーチンでアプリのパフォーマンスを改善するもご覧ください。