12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Google CodeLabs Using Kotlin Coroutines in your Android Appを日本語で概要解説

Last updated at Posted at 2019-06-01

前回Roomをお勉強したのの続きです。
Kotlin勉強するなら非同期はコルーチンでやりたいよね。Rxは勉強することが多すぎるし。
ってことで、最初はまずCodeLabsのUsing Kotlin Coroutines in your Android Appでお勉強を始めます。

日本語は大いに意訳、要約です(でも面倒になるとGoogleさんに力を借りた直訳風になりますwあと、口語になったり文語になったり)。必ず原文と照らし合わせながら参照してください。
日本語訳以外に、個人的に嵌まったところ(CodeLab内で触れていなくて罠になっているところ)も覚え書きしていきます。
斜体部分が、個人的な感想、メモ、意見、独り言です。

対象者

  • Java,Kotlinを読める
  • Android Architecture Components(以下AAC)のViewModel 、LiveDataについて何となく知っている
  • 非同期処理についてだいたい理解している(MainThreadとWorkerThreadの違いが分かる)

CodeLab説明

1. Introduction(紹介)

このCodeLabを勉強すると、Kotlin Coroutinesの使い方が分かるようになります。Androidに於けるバックグラウンド非同期処理の新しい書き方で、コールバックの嵐になっていた非同期処理をシンプルに、直列的に書けるようになります。

AACを用いて作成された、コールバックタイプの非同期処理で書かれたサンプルプロジェクトを元にして学習します。

この学習を終えれば、非同期API処理をコルーチンで書き換えられるくらいには経験を積めるでしょう。コルーチンの主要な使い方を学び、テストをどう書くかについても学べるでしょう。

What you'll learn(何が学べるの)

  • コルーチンで書かれたコードを呼び出して結果を得る方法
  • suspend functonを使用して非同期コードを直列的に書く方法
  • launchrunBlockingを使用してコードが実行される方法を制御する方法
  • suspendCoroutineを使用して既存のAPIをコルーチンに変換する方法
  • AACででコルーチンを使用する方法
  • コルーチンをテストするためのベストプラクティス

Prerequisites(前提条件)

  • AACのViewModel, LiveData, Repository, and Roomについて知っている
  • 拡張関数や、ラムダについても含む、Kotlin文法を知っている
  • 旧来のAndroidに於けるスレッドの概念を理解している(メインスレッドとワーカースレッドについて)。また、コールバックの書き方を知っている

What you'll need(必要環境)

  • Android Studio 3.3以上

2. Getting set up(準備)

Zipをダウンロードするか、下記プロジェクトをGitからクローンする。

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

3. Run the starting sample app(最初のサンプルアプリを実行しよう)

  1. zipをダウンロードした場合は、まず解凍
  2. Android Studioでkotlin-coroutines-startプロジェクトを開く
    Gradle Plugin等のバージョンを上げるか聞かれた場合、 上げない 方が良いです。本筋で無いところで修正が必要になることがあります。
  3. Runボタンをクリックして実行。(Lollipop以上の端末かエミュレーターが必要。つまりminSDK=21)

このstartアプリは、ユーザーが画面のどこかをタップした1秒後にSnackbarを表示するのに、スレッドを使っています。やってみると、*"Hello, from threads!"*と僅かな遅延後に表示されます。まず始めに、この処理をコルーチンを使用した書き方に変換します。

このアプリはAACを用いて、MainActivityのUIコードと、MainViewModelのアプリケーションロジックとを分離しています。このような構成のプロジェクトに馴染めるよう、少し時間を取ってください。

  1. MainActivityは画面表示を担当。クリックリスナーを登録したり、スナックバーを表示したり。イベントはMainViewModelに渡し、表示の更新は、MainViewModelの持つLiveDataを基に行う
  2. MainViewModelonMainViewClickedでイベントをハンドリングし、MainActivityとのやりとりはLiveDataを通して行う
  3. ExecutorsBACKGROUNDを定義。これによりバックグラウンドスレッドで実行が出来る
    BACKGROUNDは、ExecutorServiceクラスののグローバル変数ですね。
    あまり一般的に見るスレッド処理ではない気がするけど・・・それとも、これが標準なのかな?私は簡単な物はThread(Runnable)で済ましちゃいますが・・・
  4. MainViewModelTestでViewModelのテストを実装しています。

Adding coroutines to a project(コルーチンをプロジェクトに追加する)

Kotlinでコルーチンを使用するには、coroutines-coreライブラリをbuild.gradleに含める必要があります。このプロジェクトではもうやってあげちゃってるけどね。

Androidにおけるコルーチンは、コアライブラリ、Android拡張として用意されています。

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

このプロジェクトはもう入れてあげてあるから必要ないけど、自分のプロジェクトに入れるときには、appモジュールbuild.gradleにdependenciesを追記してください。

2019/05/18時点での最新バージョンは下記の通りです

app/build.gradle
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0'

[補足の内容]
RxJavaを既に自分のプロジェクトで使っている人は、RxJavaでコルーチンを使えるようにしたライブラリ kotlin-coroutines-rxが使えます。

4. Coroutines in Kotlin(Kotlinでコルーチン)

Androidにおいては、メインスレッド*(=UIスレッド)*を以下にブロッキングしないかと言うことはとても大事。メインスレッドはシングルスレッドですべてのUIの更新を制御しています。クリックやその他のUIコールバックも呼び出します。良好なユーザーエクスペリエンスを提供するにはこのスレッドがスムースに実行されていなければなりません。

メインスレッドは16msかそれ以上の頻度で、UIを更新できなければなりません。これは60FPSに相当します。これより時間のかかる処理は普通にあります。例えばでかいJSONファイルをparseするとか、デカいデータセットをデータベースに書き込むとか、ネットワークからデータを取得するとか。そんなコードをそのままメインスレッドで呼んでしまうと、アプリの反応が悪くなったり、カクついたり、最悪はフリーズします。メインスレッドの処理を長くブロックしてしまうと、アプリケーションはクラッシュして**ANR(Application Not Responding dialog)**ダイアログを表示します。
「アプリケーションXXは反応していません〜」というダイアログですね

コルーチンがこれをどうやって解決するかの紹介は、動画を観てね。
動画の内容は頑張って理解して下さい

The callback pattern(コールバックパターン)

重い処理を別スレッドで動かすパターンの一つの実際として、コールバック手法があります。バックグラウンドスレッドでコールバックを使うと、処理が終了したときに結果をメインスレッドで受け取ることが出来ます。

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

// コールバックと重い処理
@UiThread
fun makeNetworkRequest() {
    // 遅いネットワークリクエストは別スレッドで動く
    slowFetch { result ->
        // 結果が返ったら, コールバックがそれを受け取る
        show(result)
    }
    // slowFetchメソッドを呼び出したら、結果を待たずに抜けます
}

AsyncTaskとAsyncTaskLoaderを使ったサンプルの方が、Androiderには馴染みが深い気がするけど、その記述はややこしいからこのサンプルなのかな・・・

@UiThreadアノテーションが付いているので、このメソッドはメインスレッド上で素早く終了する必要があります。しかしslowFetchは遅いので、メインスレッドはその処理を待っていることは出来ません。show(result)をコールバックにすることで、slowFetchをバックグラウンドスレッドで実行し、結果を終了時に取得することを可能にしています。

Using coroutines to remove callbacks(コルーチンを使ってコールバックを削除する)

コールバック手法は悪くないんだけど、コードが読みづらくなるんだよね。それに、例外処理などの一部はコールバックで受け取れません。

Kotlinコルーチンでは、コールバックスタイルのコードを、直列的に変換できます。直列的なコードは基本的に読みやすく、例外なども使えるようになります。

最終的には両者は同じことをしています。長い処理の結果を待ち、処理を続けることが出来ます。でもコードの見た目は全然異なります。

suspendキーワードは、関数をコルーチン向けにマークするKotlin文法です。コルーチンがsuspendキーワードの付いた関数を呼んだとき、関数が戻るまでメインスレッドをブロックする代わりに、結果が戻るまで中断し、結果が出たら中断したところから処理を再開します。結果を待っている間は、他の関数やコルーチンの処理が出来るようにそちらのブロックを解除します。

下の例では、makeNetworkRequest()slowFetch()は共にsuspend関数です。

// 重い処理をコルーチンで
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch もsuspend関数で、メインスレッドをブロックする代わりに
    // makeNetworkRequest を 結果が戻るまで`suspend` する
    val result = slowFetch()
    // 結果が届いたら次の処理を続ける
    show(result)
}

// コルーチンを使ったmain-safeなメソッド
suspend fun slowFetch(): SlowResult { ... }

main-safeってメインスレッドをブロックしない、くらいの意味かな?

コールバックバージョンと同じように、makeNetworkRequest@UiThreadが付いているためメインスレッドに即座に戻る必要があります。つまり、通常はslowFetchのようなブロッキングメソッドを呼ぶことは出来ません。ここがsuspendキーワードの魔法のようなところです。

[補足内容]
重要: suspendキーワードは、実行されるスレッドを特定しません。バックグラウンドスレッドかも知れないし、メインスレッドかも知れません

コールバックの時のコードと比べると、コルーチンでのコードは、直列的に書けるため、より少ないコードで同じ結果が得られます。なので複数の非同期処理があっても、コールバック地獄にならずに済み、完結に書くことが出来ます。例えば、ネットワークの複数のエンドポイントからデータを取得してデータベースに保存するような処理も、コールバックを使わないで一連の処理のように書くことが出来ます。

// コルーチンを使ってネットワークからデータを取得してデータベースに保存する

// @WorkerThread が付いているので、この関数はメインスレッドから呼べない。エラーになる
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch と anotherFetch は suspend 関数
    val slow = slowFetch()
    val another = anotherFetch()
    // save は普通の関数で大抵スレッドをブロックする
    database.save(slow, another)
}

// コルーチンを使ったmain-safeなメソッド
suspend fun slowFetch(): SlowResult { ... }
// コルーチンを使ったmain-safeなメソッド
suspend fun anotherFetch(): AnotherResult { ... }

次のセクションでstartサンプルをコルーチンに書き換えていきます。

4. Controlling the UI with coroutines(コルーチンでUIを制御する)

このエクササイズでコルーチンを使って数秒遅れにメッセージを出します。AndroidStudioでkotlin-coroutines-startを開いて下さい。

Add a coroutine scope to MainViewModel(MainViewModelにコルーチンscopeを追加する)

Kotlinでは、コルーチンはすべてCoroutineScope内で実行されます。スコープはジョブを通してコルーチンの寿命を制御します。スコープのジョブをキャンセルしたとき、スコープ内で開始されたすべてのコルーチンがキャンセルされます。Androidで、ユーザーが画面を離れたときに処理をすべてキャンセルするのに、スコープが便利です。スコープにより、デフォルトのディスパッチャーを使うことが出来ます。ディスパッチャーは、コルーチンを実行するスレッドを制御します。

MainViewModelでコルーチンを使うために、まずスコープを次のように作成します。

private val viewModelJob = Job()

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

この例では、uiScopeは、AndroidでのメインスレッドであるDispatchers.Mainでコルーチンを開始します。メインスレッドで開始されたコルーチンはsuspendされている間スレッドをブロックしません。ViewModelのコルーチンはだいたいいつもメインスレッド上のUIを更新するのに使われるので、メインスレッドで実行されることがデフォルトなのは妥当です。このCodeLabの後半でやりますが、コルーチンは開始された後でもいつでもディスパッチャーを変更することが出来ます。例えば、メインディスパッチャーで処理を開始した後、Jsonなんかの重いパースをするときには別のディスパッチャーを使うなど出来ます。

[補足内容]
CoroutineContextについて
CoroutineScopeはCoroutineContextをパラメーターに取ることが出来ます。CoroutineContextは、コルーチンを構成する一連の属性です。 スレッド化ポリシー、例外ハンドラなどを定義できます。

上の例では、CoroutineContext plus演算子を使用して、スレッド化ポリシー(Dispatchers.Main)とジョブ(viewModelJob)を定義しています。 結果のCoroutineContextは、両方のコンテキストの組み合わせです。

直訳で済みません。Googleさんありがとう

Cancel the scope when ViewModel is cleared(ViewModelがクリアされたらスコープをキャンセルする)

ViewModelクラスが破棄されるとき、onClearedが呼ばれます。だいていユーザーが画面を離れるときはスコープの処理をキャンセルしたいものでしょう。次のようにします。

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

viewModelJobuiScopeで起動されていたので、viewModelJobがキャンセルされたときは同時にuiScopeで起動されたすべてのコルーチンがキャンセルされます。不要になったコルーチンがキャンセルできることは、バックグラウンドでの不必要な処理やメモリリークを防ぐために重要です。

重要
スコープ内で開始されたすべてのコルーチンをキャンセルするには、CoroutineScopeにJobを渡す必要があります。 そうでない場合、スコープはアプリが終了するまで実行されます。 それが意図したものではない場合、メモリがリークしていることでしょう。

CoroutineScopeコンストラクタで作成されたスコープは暗黙のジョブを追加します。これはuiScope.coroutineContext.cancel()を使用して取り消すことができます。

Use viewModelScope to avoid boilerplate code(定型コードの繰り返しを避ける為にviewModelScopeを使う)

これまでのコードを、全部のViewModelクラスに書くことになるけど、それは当然大量のボイラープレート(繰り返し定型コード)を産んでしまいます。そこで、ライブラリlifecycle-viewmodel-ktxの出番です。このライブラリを使うには、appモジュールの`build.gradleに以下のように追記します。でも実はもうこのステップもプロジェクトに入ってるけどね。

app/buidl.gradle
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-alpha02"

2019/05/18現在の最新版は2.2.0-alpha01みたいですが、それに上げるとビルドが通らなくなります。

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

すべてのViewModelクラスに新しいスコープを作るコードを書くことなく、viewModelScopeを使うことが出来ます。ライブラリが初期化や破棄のすべての面倒を見てくれます。

次のコードは、viewModelScopeを使ってバックグラウンドスレッドでネットワークリクエストを行うコルーチンを起動する方法です。

class MainViewModel : ViewModel() {
    // UIスレッドをブロックすること無くネットワークリクエストを送る
    private fun makeNetworkRequest() {
       // viewModelScope でコルーチンを起動する
        viewModelScope.launch(Dispatchers.IO) {
            // slowFetch()
        }
    }

    // onCleared()のオーバーライドは不要
}

なんかただのサンプルコードと、プロジェクトを書き換えるコードなのかが分かりづらいですね

Switch from threads to coroutines(スレッドをコルーチンに置き換える)

MainViewModel.ktの"TODO"コメントを見つけてください。

/**
* Wait one second then display a snackbar.
*/
fun onMainViewClicked() {
   // TODO: Replace with coroutine implementation
   BACKGROUND.submit {
       Thread.sleep(1_000)
       // use postValue since we're in a background thread
       _snackBar.postValue("Hello, from threads!")
   }
}

このコードはバックグラウンドスレッドで実行するためにBACKGROUND *(ExecuterServiceのインスタンス)*を使用しています。sleepは現在のスレッドをブロックするので、もしこれがメインスレッドから呼ばれていたら、UIの処理はその間固まってしまいます。画面をタップして1秒後、snackbar表示をリクエストしています。

コルーチンのコードには次のように書き換えます。launchdelayをimportしてね。

/**
* Wait one second then display a snackbar.
*/
fun onMainViewClicked() {
   // viewModelScopeでコルーチンを起動
   viewModelScope.launch {
       // 1秒間このコルーチンを中断(suspend)
       delay(1_000)
       // main dispatcherを再開
       // _snackbar.value はメインスレッド上からなら直接値を書き換えられる
       _snackBar.value = "Hello, from coroutines!"
   }
}

このコードは1秒待ち、snackbarを表示するという、同じ事をしています。しかし重要な違いがあります。

  1. viewModelScope.launchviewModelScopeでコルーチンを開始します。viewModelScopeに渡したジョブがキャンセルされるとき、このスコープ/ジョブはすべてキャンセルされることを意味します。ユーザーがActivityをdelayが戻る前に離れた場合、ViewModelのデストラクタにおいてonClearedが呼ばれた際にこのコルーチンは自動的にキャンセルされます。
  2. viewModelScopeはデフォルトのディスパッチャーとしてDispatchers.Mainを保持していることから、このコルーチンはメインスレッドで呼ばれます。他のスレッドを使う方法は後ほど見ていきます。
  3. delay関数はsuspend関数です。Android Studioの左側にアイコン[Suspend function call]が表示されます。このコルーチンはメインスレッドで実行されるものの、delay関数はスレッドを1秒間ブロックするわけではありません。代わりに、ディスパッチャーが1秒後にこのコルーチンを再開することをスケジューリングします。

実行してみましょう。画面をタップした1秒後にスナックバーが表示されるはずです。

なぜかこのタイミングで、私の環境ではMETA-INF/xxxxのビルドエラーが起きるようになりました。対処法については、前回の記事を参照下さい。

次のセクションでは、この関数をテストする方法について考えます。

6. Testing coroutines through behavior(コルーチンのテスト)

ここまで書いたコルーチンのコードのテストの書き方を学びます。スレッドを使っていたときと同じようなスタイルで書けることが分かるでしょう。このCodeLabの後半では、コルーチンと直接対話するテストの書き方も示します。

[補足の内容]
kotlinx-coroutines-testというライブラリが、experimentalで開発進行中なようです。
CodeLabでは、ライブラリがexperimentalであるため、stableなAPIを使った書き方を中心にしています。
セクションの最後にはkotlinx-coroutines-testライブラリでの書き方も載せておきます。
kotlinx-coroutines-testライブラリがstableになったらこの辺の記述が変わりそうですね。

Review the existing test(既存のテストのレビュー)

androidTestフォルダのMainViewModelTest.ktを開いて下さい。

コードは割愛します。

各テストの実行前に、2つのことが起こります。

  1. JUnitでのテストの前後に実行するコードについてのルールに、InstantTaskExecutorRuleが使われます。InstantTaskExecutorRuleはテストの実行中にLiveDataがメインスレッドにすぐにポストされる設定のルールです。
  2. setup()メソッドの中で、subjectフィールドは新しいMainViewModelに初期化されます。

初期化の後、一つのテストが定義されています。
コードは割愛します。

このテストは、onMainViewClickedを呼び、snackbar (MainViewModelのLiveDataなやつのことです。実際にActivityに表示されるSnackbarのことではありません) の値が変わるのを待ちます。その時ヘルパークラスのassertSendsValuesを使います。これはLiveDataにデータが送られるまで2秒間待つようになってます。このメソッドの中を読む必要はこのCodeLabではありません。

このテストはViewModelのpublic APIにのみ依存しています。onMainViewClickedがクリックされたとき、snackbarには*"Hello, from threads!"*が送られるはずです。

我々はpublic APIを変更してはいません。このメソッド呼び出しは相変わらずsnackbarを更新します。つまり、コルーチンに書き換えることは、既存のテストコードを破壊はしません。
とはいえ既存のテストの書き方によるとは思うけど

Run the existing test(既存のテストを実行する)

1.〜3. はテストの実行の仕方なので割愛します。

テストは、これまでのコードを実装していると、次のような結果で失敗します。

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

Update failing test to pass(失敗したテストを成功するように更新する)

メソッドを挙動を書き換えているので、テストは失敗したのです。"Hello, from threads!" ではなく、"Hello, from coroutines!"にしました。

assertionを書き換えて、メソッドの新しい挙動に合わせます。
文字列の部分を置き換えるだけなのでコードは割愛します。

テストを再実行すると、テストはパス *(=成功)*するはずです。

public APIのみでテストすることで、テストの構造を変更することなく、テストをバックグラウンドスレッドからコルーチンに変更することができました。

次に、コールバックAPIを用いたコードをコルーチンに書き換える方法について学びます。

補足内容は省きます。いずれこっちが本筋になりそうですけど。Rxっぽく書けるようになるのかな?

7. Converting existing callback APIs with coroutines(既存のコールバックAPIをコルーチンに置き換える)

コールバックAPIで書かれたコードをコルーチンに置き換える方法について学びます。

Android Studioでkotlin-coroutines-repositoryプロジェクトを開いて下さい。
startプロジェクトと同じく、Gradle Plugin等のアップデートはしない方が良いです

このアプリもAACを使い、ネットワークとデータベースを使うデータレイヤーを実装するよう前回までのプロジェクトを拡張しています。画面がタップされたら、新しいタイトルをネットワークから取得し、データベースに保存後、画面に表示します。新しいクラスをざっと眺めましょう。

  1. MainDatabase Roomを使ったデータベースで、Titleを保存、及び読み出す
  2. MainNetwork 新しいタイトルをネットワークAPIから取得する。タイトルの取得には、FakeNetworkLibrary.ktで定義されている偽装ネットワークライブラリを使う。このライブラリはランダムにエラーを返す
  3. TitleRepository ネットワークとデータベースからのデータを組み合わせて、タイトルを取得または更新するための単一のAPIを実装
  4. MainViewModelTest MainViewModelのテスト
  5. FakeNetworkCallAwaitTest テストコード。このCodeLabの後半で完成予定

Explore the existing callback API(既存のコールバックAPIについて)

MainNetwork.ktを開きfetchNewWelcome()の定義を見て下さい。
コードは割愛します。

TitleRepository.ktを見ると、fetchNewWelcome()がコールバックパターンを用いてネットワーク呼び出しをするのにどう使われているかが分かります。

この関数はFakeNetworkCallを返します。それに対してコールバックを登録できます。fetchNewWelcomeの呼び出しは遅く長いネットワーク処理を別スレッドで行い、addOnResultListenerに結果オブジェクトを返します。addOnResultListenerで渡したコールバックが、要求が完了したとき、またはエラーが発生したときに呼び出されます。

直訳すると分かりづらいけど、コールバックスタイル=addOnXXXXXListenerみたいにイベントリスナーを登録していく方式、ということで、Androidの既存の実装ではお決まりの手法ですね。確かにこのコードは結構読みづらくなるんですよね。ラムダとかでだいぶ変わりはしますが、それでも連続処理したいときなんかは非常にネストが深くなって面倒です。それをコルーチンなら!ということです。

Convert the existing callback API to a suspend function(コールバックAPIをsuspend functionに書き換える)

refreshTitle関数は、この時点では、FakeNetworkCallのコールバックを使用しています。この練習でのゴールは、ネットワークAPIの呼び出しを、susnpend関数に変えることでrefreshTitleをコルーチンで書けるようになることです。

コールバックで書かれたAPI呼び出しをsuspend関数に変換できるsuspendCoroutineがKotlinには用意されています。

suspendCoroutineを呼び出すと、直ちにカレントコルーチンが中断されます。suspendCoroutineはそのコルーチンを再開するためのcontinuationオブジェクトを返します。continuationは文字通りのことをします。すなわち、中断されたコルーチンを継続、中断するのに必要なすべてのコンテキストを保持することです。

suspendCoroutineが提供するcontinuationには2つの関数があります。resumeresumeWithExceptionです。どちらの関数も呼び出しが行われると直ぐにsuspendCoroutineが再開されます。

suspendCoroutineは、コールバックを待つ前に使ってコルーチンを中断します。コールバックが呼ばれたらその戻り値を使って再開するのに、resumeresumeWithExceptionを呼びます。

suspendCoroutineのサンプルは次のようになります。


// suspendCoroutineサンプル

/**
 * 文字列をコールバックするクラス
 */
class Call {
  fun addCallback(callback: (String) -> Unit)
}

/**
 * コールバックベースのAPIをsuspend関数として公開し、コルーチンで使用できるようにする
 */
suspend fun convertToSuspend(call: Call): String {
   // 1: suspendCoroutineはすぐにコルーチンを *suspend* する
   // そのブロックから渡されたcontinuationオブジェクトからのみ *resumed* される
   return suspendCoroutine { continuation ->
       // 2: suspendCoroutineをコールバック登録のために渡す

       // 3: コールバックを渡して結果を待つ
       call.addCallback { value ->
           // 4: use continuation.resume to *resume* the coroutine
           with the value. The value passed to resume will be
           the result of suspendCoroutine.
           // 4: continuation.resumeで コルーチンを *resume* する。
           // 値valueはsuspendCoroutineの結果である
           continuation.resume(value)
       }
   }
}

この例はsuspendCoroutineがAPIベースのコールバックCallをsuspend関数で使う方法を示しています。こうすると、Callを直接次のように使うことが出来ます。

// コールバックAPIをコルーチンで使うためのconvertToSuspendの使い方のサンプル

suspend fun exampleUsage() {
    val call = makeLongRunningCall()
    convertToSuspend(call) // 長い処理が終わるまでsuspendする
}

このパターンを使ってFakeNetworkCallでsuspend関数を公開することができます。これによりコルーチンでコールバックベースのネットワークAPIを使うことができます。

この例ははっきり言って良く分からない・・・


What about cancellation?(キャンセルについて)

キャンセルを意識する必要が無いときは、suspendCoroutineが良いでしょう。しかし、通常はキャンセルを考慮する必要があります。そういう場合はsuspendCancellableCoroutineを使えばいいでしょう。


Use suspendCoroutine to convert a callback API to coroutines(suspendCorutineを使ってコールバックAPIをコルーチンに置き換える)

TitleRepository.ktを下までスクロールすると、TODOコメントがあります。

そこを下記のようにFakeNetworkCall<T>の拡張関数を定義します。

suspend fun <T> FakeNetworkCall<T>.await(): T {
   return suspendCoroutine { continuation ->
       addOnResultListener { result ->
           when (result) {
               is FakeNetworkSuccess<T> -> continuation.resume(result.data)
               is FakeNetworkError -> continuation.resumeWithException(result.error)
           }
       }
   }
}

kotlin.coroutinesのimportを、間違えてexperimentalの方にしないように!

この拡張関数は、suspendCoroutineを用いて、コールバックベースのAPI処理をsuspend関数に変換します。コルーチンはawaitを呼び出して直ぐに中断しネットワーク呼び出しの結果が届くまで待つことが出来ます。結果はawaitの戻り値として返され、エラー時は例外が発生します。

このコードは次のように使うことが出来ます。

// awitの使い方サンプル

suspend fun exampleAwaitUsage() {
   try {
       val call = network.fetchNewWelcome()
       // fetchNewWelcomemが結果を返すかエラーを投げる課するまでsuspendする
       val result = call.await()
       // resumeにより、awaitが結果を返す
   } catch (error: FakeNetworkException) {
       // resumeWithExceptionは、awaitに例外を投げる
   }
}

await関数の宣言を見みてみます。suspendキーワードは、Kotlinに対しコルーチンで使えることを示しています。その結果、他のsuspendCoroutineのようなsuspend関数を呼ぶことが出来ます。残りの、fun <T> FakeNetworkCall<T>.await()部分のでは、FakeNetworkCallクラスのオブジェクトすべてにawaitという名の拡張関数を定義していることを意味します。FakeNetworkCallクラスを変更する物ではないですが、Kotlinから呼ばれたとき、これはpublic関数として扱われます。awaitの戻り値はTです。


What is an extension function?(拡張関数って?)
Kotlin初心者なら、拡張関数について初めて触れるかも知れません。拡張関数はクラスを書き換えるものではありませんが、thisを第一引数として受け取れる、新しい関数を導入することが出来る仕組みです。

fun <T> await(this: FakeNetworkCall<T>): T

await関数に対して、thisにはFakeNetworkCall<T>がバインドされて渡されます。だからそのメンバーメソッドであるaddOnResultListenerawait内で呼び出せるのです。


まとめると、このシグネチャ(宣言)は、本来コルーチン用に構築されたのではないクラスに、await()というsuspend関数を追加していることを意味します。 このアプローチは、実装を変更せずにコルーチンをサポートするためにコールバックベースのAPIを更新するために使用できます。

次のエクササイズでは、await()のテストを書き、テストから直接コルーチンを呼ぶ方法について学びます。

8. Testing coroutines directly(コルーチンを直接テストする)

このエクササイズでは、suspend関数を直接呼び出すテストの書き方を学びます。

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

前回作成した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)
           }
       }
   }
}

Write a test that calls a suspend function(suspend関数を呼ぶテスト)

androidTestフォルダのFakeNetworkCallAwaitTest.ktファイルを開いて下さい。二つの TODOがあります。

二つ目のテスト、whenFakeNetworkCallFailure_throwsawaitを呼んでみてください。

@Test(expected = FakeNetworkException::class)
fun whenFakeNetworkCallFailure_throws() {
    val subject = makeFailureCall(FakeNetworkException("the error"))

    subject.await() // Compiler error: Can't call outside of coroutine
}

awaitはsuspend関数ですから、Kotlinはコルーチンや他のsuspend関数以外からそれを呼び出す手段がありません。というわけで、これではコンパイルエラーになります。エラーメッセージは*"Suspend function 'await' should be called only from a coroutine or another suspend function."*という感じでしょう。

test runnerはコルーチンについては何も知りません。なのでこのテストをsuspend関数にすることは出来ません。ViewModelでやったように、CoroutineScopeを使ってコルーチンを起動することは出来ますが、テストは、テストメソッドが戻る前にコルーチンの処理が終了しなければなりません。テスト関数が戻れば、テストは終わってしまいます。launchで起動されたコルーチンは非同期処理なので、未来に終了します。(テストメソッドが戻った時点で終わっていると限らない) 非同期処理のテストをするには、どうにかして、テストにコルーチンの処理の終了を待たせなくてはなりません。launchはnon-blockingなので、呼ばれるとただちに戻り、関数が戻った後もコルーチンの処理を継続できるわけで、テストで使う事は出来ません。例えば、

// launchを使ったテストのサンプル(常に失敗)

@Test(expected = FakeNetworkException::class)
fun whenFakeNetworkCallFailure_throws() {
    val subject = makeFailureCall(FakeNetworkException("the error"))

   // launchはコルーチンを開始し、直後に戻る
   GlobalScope.launch {
       // これは非同期処理なので、テストメソッドが完了した後に呼ばれる可能性がある
       subject.await()
   }
   // test関数は直ぐに終了するため、await()の起こした例外を検知できない
}

このテストは常に失敗します。launchの呼び出しは直ぐに終わり、このテストケースも終了します。awaitからの例外は、テストの終了前にも、後にも起こりえますが、例外オブジェクトはtestのコールスタックへは投げられません。コルーチンのスコープ内の未処理例外ハンドラに投げられてしまいます。

KotlinにはrunBlockingという関数があります。これはsuspend関数が呼ばれている間、その関数の処理をブロックします。runBlocking内でsupend関数を呼ぶと、通常は中断される処理を、普通の関数の実行のようにブロックすることが出来ます。suspend関数を通常関数の呼び出しのように変換できる手法と考えてよいでしょう。

runBlockingはコルーチンをあたかも普通の関数のように呼ぶので、例外もまた普通の関数のように投げます。


Important(重要)
runBlockingは呼び出したスレッドをブロックします。コルーチンは同じスレッド内で同期的に処理されます。だから普通のアプリの中では使っちゃダメだよ。launchを使ってね。
runBlockingはAPIのテストみたいな時にだけ使いましょ。


補足内
前述のkotlinx-coroutines-test ライブラリに、runBlockingTestというのが追加されているようです。

await呼び出しをrunBlockingで囲みましょう。

@Test(expected = FakeNetworkException::class)
fun whenFakeNetworkCallFailure_throws() {
   val subject = makeFailureCall(FakeNetworkException("the error"))

   runBlocking {
       subject.await()
   }
}

一つ目のテストも同じようにrunBlockingを使って実装します。

@Test
fun whenFakeNetworkCallSuccess_resumeWithResult() {
   val subject = makeSuccessCall("the title")

   runBlocking {
       Truth.assertThat(subject.await()).isEqualTo("the title")
   }
}

テストを実行してみましょう。2つとも通過するはずです!

次のエクササイズでは、RepositoryViewModelからデータをfetch(取り出す)のにコルーチンを使う方法について学びます。

9. Using coroutines on a worker thread(ワーカースレッドでコルーチンを使う)

ここでは、スレッドでの処理をコルーチンに変更する方法について学びます。これによりTitleRepositoryの実装を完成できます。

Review the existing callback code in refreshTitle(refreshTitleの既存のコールバックの実装を見る)

TitleRepository.kt

fun refreshTitle(onStateChanged: TitleStateListener) {
   // 1: ネットワークリクエスト開始
   onStateChanged(Loading)
   val call = network.fetchNewWelcome()
   // 2: ネットワークリクエストの完了やエラーを受け取るためにコールバックを登録
   call.addOnResultListener { result ->
       when (result) {
           is FakeNetworkSuccess<String> -> {
               // 3: 新しいtitleをバックグラウンドスレッドで保存
               BACKGROUND.submit {
                   // insertTitleはバックグラウンドスレッドで実行
                   titleDao.insertTitle(Title(result.data))
               }
               // 4: 呼び出し元にリクエストの成功を通知
               onStateChanged(Success)
           }
           is FakeNetworkError -> {
               // 5: 呼び出し元にクエストの失敗を通知
              onStateChanged(
                  Error(TitleRefreshError(result.error)))
           }
       }
   }
}

TitleRepository.ktrefreshTitleメソッドは、呼び出し元とのネットワークリクエストのローディングやエラー状態の通知をやりとりするのにコールバックを用いて実装されています。二つのコールバックがあり、コードの可読性を下げています。何をやっているかは次の通りです。

  1. リクエストを開始する前に、コールバックはリクエストがLoading状態になったことを通知する
  2. ネットワーク処理の結果を待つため、FakeNetworkCallにコールバックを登録
  3. 新しいtitleがネットワークから取得できたら、バックグラウンドスレッドにてDBに保存
  4. 呼び出し元にリクエストの完了(及びLoading状態ではなくなったこと)を通知
  5. リクエストが失敗したときは、呼び出し元にエラー(及びLoading状態ではなくなったこと)を通知

MainViewModel.ktを開いて、このAPIがどのようにUIの表示制御に使われているか見てみましょう。

MainViewModel.kt
fun refreshTitle() {
   // stateリスナーをrefreshTitleにラムダで渡す
   repository.refreshTitle { state ->
       when (state) {
           is Loading -> _spinner.postValue(true)
           is Success -> _spinner.postValue(false)
           is Error -> {
               _spinner.postValue(false)
               _snackBar.postValue(state.error.message)
           }
       }
   }
}

refreshTitleを呼び出す側のコードは、それほど複雑ではありません。repository.refreshTitleにコールバックを渡し、Loading, Success, Errorのいずれかの状態に変わるたびに繰り返し呼び出されます。どのケースでも、適切なLiveDataによりUIが更新されます。

Replace callback code with coroutines in TitleRepository(TitleRepositoryのコールバック実装をコルーチンに変更する)

TitleRepository.ktを開き、refreshTitleをコルーチンを用いた書き方に変更します。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)
       }
   }
}

このコードは前回fetchNewWelcomeをsuspend関数に変更したときに定義したawaitを使っています。awaitはネットワークリクエストの結果をresume時に返しますから、コールバックを作る必要も無く、resultに直接代入することが出来ます。ネットワークリクエストがエラーになった場合は、awaitは例外を投げます(resumeWithExceptionを呼んでるので)。従って、通常のtry/catchブロックで捉えることが出来るわけです。

withContext関数は、データベースへの挿入処理がバックグラウンドスレッドで行われるようにするために使われています。insertTitleは処理をブロックするため重要なことです。例えコルーチンの中で動いていても、処理が終わるまでそのコルーチンが走っているスレッドをブロックしてしまうのです。insertTitleをメインスレッドから呼ぶと、例えコルーチンを使っていても、データベースの書込が終わるまで、UIがフリーズする原因となるでしょう。

withContextを使うと、コルーチンは、指定されたディスパッチャーにブロック処理を受け渡します。ここで指定したDispatchers.IOは、データベースなどのディスクIO向けに設計された巨大なスレッドプールです。withContextが終了したとき、コルーチンはその前に指定されたディスパッチャーで処理を継続します。これはスレッドを短期的に切り替えて実行するとても良い例です。特にディスクIOやCPU集中タスク(大量の計算処理とかのこと?)等のメインスレッドから実行されるべきでない処理を行う場合に有効です。

ここではスコープの指定を必要としていません。なぜなら、ここではコルーチンを起動していないからです。この関数は呼び出し元のコルーチンのスコープにおいて実行されます。

新しいコードでは、ローディングステータスをもはや投げていないことに気付きましたでしょうか。MainViewModelをこのsuspend関数を使うように変更したら、コルーチンでの実装においては明示的である必要が無いことが分かるでしょう。


Prefer suspend functions for Kotlin APIs(suspend関数を使う方が良い)
拡張関数awaitを定義するのは、既存のAPI呼び出しをコルーチンに書き換えるときにはまあまあ良い方法です。でも、これはすべてのAPI呼び出しでawaitを使わなければならないという制約になります。(結局その書き換えが生じる)
新しくAPI呼び出しを作るなら、そもそもsuspend関数を直接使うべきです。
そうすれば全API呼び出しでawaitを呼び出さなくても良くなります。


Use suspend function in MainViewModel(MainViewModelでsuspend関数を使う)

MainViewModel.ktを開き、refreshTitleをコルーチンベースの実装に書き換えましょう。

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はrefreshが完了するまでコルーチンを中断するので、コールバックを通してステータスを渡す必要はありません。その代わり、ローディングスピナーのコントロールはViewModelのfinallyブロックで行われるようにすることが出来ます。


What happens to uncaught exceptions(未処理例外はどうなる?)
キャッチされていない例外はコルーチン以外での処理と同様、未処理例外ハンドラによってキャッチされます。コルーチンの処理はキャンセルされます。


アプリを実行してみましょう。画面のどこかをタップすると、スピナーが表示されるでしょう。タイトルがデータベースから更新されるか、あるいはエラーがスナックバーで表示されるでしょう。

次のエクササイズでは、これらのコードを汎用的なものにリファクタリングします。

10. Using coroutines in higher order functions(高階関数でのコルーチンの使用)

MainViewModelrefreshTitleをリファクタリングし、汎用的にしていきます。コルーチンを使った高階関数*(※直訳)*の作り方が分かるようになります。

refreshTitleは今の実装でも動きます。しかし、常にスピナーを表示する汎用的なデータ読み込み用のコルーチンを作ることも出来ます。これは、いくつかのイベントに応答してデータをロードし、ローディングスピナーが常に表示されるようにしたいような状況で役立ちます。

repository.refreshTitle()の1行以外は、ボイラーテンプレートであって、スピナーを表示したりエラーを表示したりする部分は、いつも同じコードになることでしょう。

コードは割愛します


Important:重要
このコードラボでは、一つのスコープviewModelScopeしか使っていませんが、一般的には適宜必要なスコープを使うことが可能です。ただそれが不要になったときにキャンセルすることを忘れないでください。例えば、RecyclerViewのAdapterでDiffUtilを操作するとき等です。


Using coroutines in higher order functions(高階関数でコルーチンを使う)

MainViewModel.ktでlaunchDataLoadと実装しろという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はsuspendであるラムダを引数blockとして受け取ります。suspendなラムダにより、suspend関数を呼ぶ事が出来ます。これが、Kotlinがこのコードラボで使用してきたコルーチンビルダーの起動とrunBlockingを実装している方法でもあります。

suspendラムダを使用するには、suspendキーワードを使います。アロー演算子に続けて、戻り値としてUnitを宣言すれば完了です。

通常、suspendラムダを自分で定義していく必要はほとんど無いですが、このように何度も繰り返される処理をカプセル化するのにはとても有効な方法です。

次のエクササイズでは、WorkManagerからコルーチンを呼び出す方法について学びます。

11. Using coroutines with WorkManager(WorkManagerでコルーチンを使う)

WorkManagerからコルーチンベースの実装をどのように使うか学びます。

What is WorkManager(WorkManagerとは)

遅延可能なバックグラウンド処理を行う方法は、Androidにおいていくつかあります。ここでは、WorkManagerとコルーチンをどのように使うか見ていきましょう。WorkManagerは遅延可能なバックグラウンド処理向けの、互換性があり、柔軟でシンプルなライブラリです。WorkManagerは、Android上のこれらのユースケースに推奨されるソリューションです。

WorkManagerはAndroid Jetpackの一部であり、適切なタイミングでの実行が保証される必要のあるバックグラウンド処理向けのAACです。([opportunistic]の訳が大変難しいですが、「タイミングが来れば直ちに」とか、「機会が訪れればすぐに」、つまりスレッドが空けば、とか、リソースが空けば、みたいな意味かと思います。ネットの翻訳だと「日和見主義」ってなるけどどう考えても違うw) 適切なタイミングでの実行とは、WorkManagerはあなたのバックグラウンド処理を、可能な限り素早く行うことを指します。実行が保証されるということは、WorkerManagerがあなたのバックグラウンド処理を様々な状況下で開始させるための面倒を見てくれると言うことになります。たとえユーザーがあなたのアプリを離れていたとしてもです。

以上のことから、WorkManagerはいずれかならず成功すべきタスクにはとても良い選択肢です。

例えば以下のようなタスクは、WorkManagerに向いているでしょう。

  • ログをアップロードする
  • 画像にフィルターを適用したり保存したりする
  • ローカルデータをネットワークと定期的に同期する

Using coroutines with WorkManager(WorkManagerでコルーチンを使う)

WorkManagerは、ListanableWorkerを基本とした、異なる実装を提供します。

最もシンプルなWorkerクラスは、いくつかの同期的な操作を行うことが出来ます。しかしながら、コルーチンとsuspend関数を使ったコードラボですから、WorkManagerを使う良い例はCoroutineWorkerを通してsuspend関数であるdoWork()を定義して使うことでしょう。

class RefreshMainDataWork(context: Context, params: WorkerParameters) :
        CoroutineWorker(context, params) {

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

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

CoroutineWorker.doWork()はsuspend関数であることに注目してください。単純なWorkerクラスと違い、このコードはWorkManager構成で指定されたExecutor上では実行されません。

※zipダウンロードした場合、プロジェクトが古いのか、上記コードをコピペしてもビルドが通りません。workライブラリのdependenciesの書き方が間違っているようので以下のように直してください。

app/build.gradle
    def work_version = "2.0.1"
    implementation "androidx.work:work-runtime-ktx:$work_version"
    // optional - Test helpers
    androidTestImplementation "androidx.work:work-testing:$work_version"

Gradle syncした後、CoroutineWorkerをimportして使えるようになります。

Testing our CoroutineWorker(CoroutineWorkerをテストする)

コードを実装したらテストを書いてこそ完成となります。

WorkManagerは、テスト方法がいくつかあります。詳しいことはドキュメントを読んで下さい。

WorkManagerのv2.1から、ListenableWorkerやCoroutineWorkerをテスト出来るシンプルなAPIセットが用意されました。ここではその新しいAPIのTestListenableWorkerBuilderを使ってみましょう。

テストを追加するには、androidTestフォルダの下に、RefreshMainDataWorkTestという名前のKotlinファイルをまず作成します。ファイルのフルパスはこうなるはずです。
app/src/androidTest/java/com/example/android/kotlincoroutines/main/RefreshMainDataWorkTest.kt

ファイルの中身は次のようにします。

RefreshMainDataWorkTest.kt
package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.util.DefaultErrorDecisionStrategy
import com.example.android.kotlincoroutines.util.ErrorDecisionStrategy
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
    private lateinit var context: Context

    @Before
    fun setup() {
        context = ApplicationProvider.getApplicationContext()


        DefaultErrorDecisionStrategy.delegate =
                object: ErrorDecisionStrategy {
                            override fun shouldError() = false
                        }
    }

    @Test
    fun testRefreshMainDataWork() {
        // Get the ListenableWorker
        val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context).build()

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

        assertThat(result, `is`(Result.success()))
    }
}

上記をコピペしただけだとビルド通りません。下記のようにdependenciesに変更、追加が必要です。

app/build.gradle
    def work_version = "2.1.0-alpha02" // 変更
    androidTestImplementation 'androidx.test:core:1.1.0' // 追加

setup関数では、テストが失敗しないようにデフォルトストラテジーを変更しています。(そうでなければランダムに通信は失敗します)(※これはこのプロジェクトで作っているものです。アプリを実行するとランダムに通信が失敗するようになっているので、これを無効化するためのコードです)

テストそのものは、TestListenableWorkerBuilderを使ってworkerを作り、startWork()メソッドを呼び出しています。

WorkManagerはコルーチンがAPI設計をいかにシンプルに出来るかの一例です。

12. Where to learn more(もっと学ぶには)

概要のまとめと発展的内容なので割愛します。

最後に

感想

ちょっと私が知りたかったこととは違った感じです。
プロジェクトの原形が、私には見慣れない形だったのでまずそっちを理解するのに消耗してしまいました。
Androidで標準とされてきた非同期処理の、AsyncTaskやAsyncTaskLoaderからの移植をしたいのでこのコードラボを見始めたのですが・・・
それとも、「標準」と思ってきたのは私だけで、世間では違ったのでしょうか?AsyncTask/AsyncTaskLoaderはGoogleさんにとってはそれほどまでに黒歴史なんでしょうか(笑)

何はともあれ、ここの情報を参考に頑張ってみます。
まあ、AsyncTask/AsyncTaskLoaderもコールバックベースではあるので、まるきり参考にならないと言うことはないでしょうが・・・・

でも、そもそもコルーチンに置き換えるならば、コールバック関数なんかも無くしたいので、やっぱりこのままは参考にならないかな。

ひとまずは、suspend functionを使えばいいのだと言うことは分かりました。
あと、とにかくテストせよ、と書いているのが興味深いですね。通信や重い処理の絡むテストは、確かに悩みどころではありますので、そこがこんなに簡単になるよ!というアピールなのでしょう。

AsyncTask/AsyncTaskLoader→コルーチンの書き換え、というかコールバックを使わないシンプルなAPIの形での書き換えが上手くいったら、別の記事でUPしたいと思います。
(コルーチン、非同期処理、で検索すると出てくる情報は、まだまだバージョンが古かったり、コルーチンがexperimentalの頃の物だったりしてだいぶ違うようなので、それで難儀しているのですが)

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?