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 [Run] ボタンをクリックして、エミュレータを選択するか、Android Lollipop が動作する Android デバイスを接続します(対応 SDK は 21 以降)。Kotlin コルーチン画面が表示されます。

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

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

  1. MainActivity は UI を表示し、クリック リスナーを登録し、Snackbar を表示できます。イベントを MainViewModel に渡し、MainViewModelLiveData に基づいて画面を更新します。
  2. MainViewModelonMainViewClicked のイベントを処理し、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 ミリ秒以上ごとに、つまり 1 秒あたり約 60 フレームで画面を更新する必要があります。サイズの大きい JSON データセットの解析、データベースへのデータの書き込み、ネットワークからのデータの取得など、多くの一般的なタスクにはこれよりも長い時間がかかります。したがって、メインスレッドからこのようなコードを呼び出すと、アプリの一時停止、途切れ、またはフリーズが発生する可能性があります。メインスレッドを長時間ブロックすると、アプリがクラッシュして、アプリケーション応答なしのダイアログが表示される場合もあります。

次の動画では、コルーチンがメインセーフティを導入することで、Android でこの問題を解決する方法について説明しています。

コールバック パターン

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

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

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

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

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

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

Kotlin コルーチンを使用すると、コールバック ベースのコードを順次コードに変換できます。通常、コードを順番に記述すると読みやすくなり、例外などの言語機能を使用できるようになります。

最終的に、コルーチンとコールバックでも同様に同じタスクが実行されます。長時間実行タスクが結果を取得するのを待ってから、タスクの実行を続行します。ただし、コードでは大きく異なります。

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

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

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

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

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

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

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

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

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

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

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

CoroutineScope について

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

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

viewModelScope の使用

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

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

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

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

MainViewModel.kt で、次の TODO と次のコードを見つけます。

MainViewModel.kt

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

このコードでは、BACKGROUND 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 の依存関係を満たすためにのみフェイクが必要です。このコードラボの後半で、コルーチンをサポートするようにフェイクを更新します。

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

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

MainViewModelTest.kt

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

onMainViewClicked を呼び出すと、作成したコルーチンが起動します。このテストでは、onMainViewClicked が呼び出された直後にタップテキストが「0 taps」のままであり、1 秒後に「1 taps」に更新されることを確認します。

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

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

既存のテストを実行する

  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 を監視して、現在のタイトルに更新されます。

コルーチンの更新では、まったく同じ動作を維持します。Room データベースなどのオブザーバブル データソースを使用して、UI を自動的に最新の状態に保つのが良いパターンです。

コルーチン バージョン

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

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

TitleRepository.kt

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

この Codelab が完了したら、Retrofit と Room を使用して新しいタイトルを取得し、コルーチンを使用してデータベースに書き込むように更新します。今のところ、500 ミリ秒間だけ作業をしているふりをして、その後続行します。

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

MainViewModel.kt

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

この関数をステップ実行してみましょう。

viewModelScope.launch {

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

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

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

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

このコルーチンは、処理を行う前に読み込みスピナーを開始し、通常の関数と同様に refreshTitle を呼び出します。ただし、refreshTitle は中断関数であるため、通常の関数とは異なる方法で実行されます。

コールバックを渡す必要はありません。コルーチンは、refreshTitle によって再開されるまで一時停止します。通常のブロッキング関数呼び出しとまったく同じように見えますが、ネットワークとデータベースのクエリが完了するまで自動的に待機し、メインスレッドをブロックせずに再開します。

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

中断関数の例外は、通常の関数のエラーと同様に機能します。suspend 関数でエラーをスローすると、呼び出し元にスローされます。実行方法は大きく異なりますが、通常の try/catch ブロックを使用して処理できます。これは、すべてのコールバックに対してカスタム エラー処理を構築するのではなく、エラー処理に組み込みの言語サポートを利用できるため便利です。

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

最後に、finally ブロックで、クエリの実行後にスピナーが常にオフになるようにします。

開始構成を選択して 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.kt では、読み込みとエラーの状態を呼び出し元に伝えるコールバックを使用して、メソッド refreshTitleWithCallbacks が実装されています。

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

  1. BACKGROUND ExecutorService で別のスレッドに切り替える
  2. ブロッキング execute() メソッドを使用して fetchNextTitle ネットワーク リクエストを実行します。これにより、現在のスレッド(この場合は BACKGROUND のスレッドの 1 つ)でネットワーク リクエストが実行されます。
  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 ベースの型を返すこともできます。それでも Retrofit の完全な Result へのアクセスを提供したい場合は、一時停止関数から String ではなく Result<String> を返すことができます。

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

Room と Retrofit を使用する

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

タイトルRepository.kt

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

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

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

コンパイラ エラーの修正

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

プロジェクトを確認し、関数を suspend に変更してコンパイラ エラーを修正します。それぞれの簡単な解決策は次のとおりです。

TestingFakes.kt

新しい一時停止修飾子をサポートするようにテスト フェイクを更新。

TitleDaoFake

  1. alt+Enter を押して、階層内のすべての関数に suspend 修飾子を追加します

MainNetworkFake

  1. alt+Enter を押して、階層内のすべての関数に suspend 修飾子を追加します
  2. fetchNextTitle をこの関数に置き換えます
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. alt+Enter を押して、階層内のすべての関数に suspend 修飾子を追加します
  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 ラムダを使用すると、suspend 関数を呼び出すことができます。これが、この Codelab で使用してきたコルーチン ビルダー launchrunBlocking を Kotlin が実装する方法です。

// suspend lambda

block: suspend () -> Unit

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

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

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

WorkManager とは

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

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

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

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

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

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

WorkManager は、さまざまなユースケースに対応するため、ベースの ListanableWorker クラスのさまざまな実装を提供します。

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

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

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

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

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

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

なお、CoroutineWorker.doWork() は suspend 関数です。よりシンプルな Worker クラスとは異なり、このコードは WorkManager の設定で指定された Executor では実行されず、代わりに coroutineContext メンバー(デフォルトでは Dispatchers.Default)のディスパッチャーを使用します。

CoroutineWorker のテスト

テストなしでコードベースを完成させることはできません。

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

WorkManager v2.1 では、ListenableWorker クラス、ひいては CoroutineWorker をより簡単にテストするための新しい API セットが導入されています。コードでは、これらの新しい API の 1 つである TestListenableWorkerBuilder を使用します。

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

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

package com.example.android.kotlincoroutines.main

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


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

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

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

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

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

}

テストの前に、WorkManager にファクトリを伝えて、偽のネットワークを挿入できるようにします。

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

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

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

取り上げた内容は次のとおりです。

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

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

詳細

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

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