araki tech

for developers including me

Android Kotlinにおける非同期処理Tips

Android Kotlinにおける非同期処理Tips

結論から

AndroidをKotlinで開発する上で以下のことを守りましょう。

  • GlobalScopeは使わない
  • データの取得・挿入はDispatchers.IOを指定する
  • Dispatchersの未指定は無くす
  • その他重い処理は非同期で行うことを視野に入れる
  • どのLifecycleScopeで非同期処理が実行されているかを気にする

ただしKotinもAndroidも日々アップデートされていくので、もしかしたら情報が古くなる可能性があります。

本記事は投稿現在 (2021.11.15) におけるTipsとして読んでください。

Kotlinの非同期処理

Kotlinで非同期処理を行う場合は簡単です。

非同期処理を開始するスコープからlaunchすれば良いだけです。

GlobalScope.launch {
    // 非同期で実行したい処理
}

他にもChannelやFlowなど、さまざまな非同期処理がKotlinではサポートされていますが、今回は主にこのlaunchに焦点を当ててお話ししていきます。

Android開発ではGlobalScopeは使わない

Android でのコルーチンに関するベスト プラクティス | Android developersには以下のようなことが書いてあります。

GlobalScope を使用しない

GlobalScope を使用すると、クラスで使用す CoroutineScope がハードコードされ、いくつかの欠点が生じます。

ハードコード値が昇格します。GlobalScope をハードコードすると、Dispatchers もハードコードする場合があります。

制御不能なスコープでコードが実行されるのでテストが非常に難しくなり、実行を制御できなくなります。

スコープ自体に組み込まれたすべてのコルーチンに対して、共通の CoroutineContext を実行することはできません。

現在のスコープより長く存続させる必要のある処理に関しては、代わりに CoroutineScope の挿入を検討してください。このトピックの詳細については、「ビジネスレイヤとデータレイヤでのコルーチンの作成」のセクションをご覧ください。

ここで大事なのは、GlobalScopeを使用すると制御不能なスコープでコードが実行されるということです。

もし、viewModelScopeでlaunchされた場合、ViewModelが破棄されればその非同期処理も止まりますが、GlobalScopeはそうではありません。

GlobalScopeアプリが生きている間のスコープですので、止まって欲しいタイミングを制御するといったことは難しくなります

結論としてActivityやFragmentでは、

lifecycleScope.launch(Dispatchers.Main) {

}

そしてViewModelでは、

viewModelScope.launch(Dispatchers.Main) {

}

のように提供されているCoroutineScopeを使うようにしましょう。

もし自身のLifecycleより長いスコープで非同期処理をしたい場合

例えば、ActivityやFragmentのonStop()などで非同期処理をはじめたい場合、自身の残ったLifecycleScopeでは処理が終わりきらない可能性があります

そう言った場合は、上記ドキュメントに書いてあるように、外部スコープとしてCoroutineScopeをプロパティで保持しておくようにします。

現在のスコープより長く存続させる必要のある処理に関しては、代わりに CoroutineScope の挿入を検討してください。

以下は公式ドキュメントのコードを転載しています。(コメントは和訳してます)

// GlobalScopeを使う代わりに外部スコープを挿入してください。
// GlobalScopeは直接使われることはありません。ここではデフォルトパラメータ定義することに意味があります。
class ArticlesRepository(
    private val articlesDataSource: ArticlesDataSource,
    private val externalScope: CoroutineScope = GlobalScope,
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    // スクリーンからユーザが離れたとしても、bookmarketingを完了させたいので、
    // 外部スコープから新しいコルーチンを作成して作業を行います。
    suspend fun bookmarkArticle(article: Article) {
        externalScope.launch(defaultDispatcher) {
            articlesDataSource.bookmarkArticle(article)
        }
            .join() // Wait for the coroutine to complete
    }
}

////////////////////////////////////
// GlobalScopeを直接使わないこと。
class ArticlesRepository(
    private val articlesDataSource: ArticlesDataSource,
) {
    // スクリーンからユーザが離れたとしても、bookmarketingを完了させたいので、
    // GlobalScopeを使って新しいコルーチンを作成する作業を行います。
    suspend fun bookmarkArticle(article: Article) {
        GlobalScope.launch {
            articlesDataSource.bookmarkArticle(article)
        }
            .join() // Wait for the coroutine to complete
    }
}

この場合、externalScopeは呼び出し元のLifecycleScopeに依存するようになります。

supervisorScopeもしくはcoroutineScopeを使うのもOKです。




データ取得・挿入はDispatchers.IOを指定する

Kotlin コルーチンでアプリのパフォーマンスを改善する | Android developersによるとDispatchersの扱いについて以下のように書かれています。

コルーチンを実行すべき場所を指定するために、Kotlin では、デベロッパーが使用できる次の 3 つのディスパッチャを用意しています。

Dispatchers.Main – このディスパッチャを使用すると、コルーチンはメインの Android スレッドで実行されます。UI を操作して処理を手早く作業する場合にのみ使用します。たとえば、suspend 関数の呼び出し、Android UI フレームワーク オペレーションの実行、LiveData オブジェクトのアップデートを行う場合などです。

Dispatchers.IO – このディスパッチャは、メインスレッドの外部でディスクまたはネットワークの I/O を実行する場合に適しています。たとえば、Room コンポーネントの使用、ファイルの読み書き、ネットワーク オペレーションの実行などです。

Dispatchers.Default – このディスパッチャは、メインスレッドの外部で CPU 負荷の高い作業を実行する場合に適しています。ユースケースの例としては、リストの並べ替えや JSON の解析などがあります。

つまり、以下の図で示すようなフローでDispachersを決めれば良いということです。

Android Kotlinにおける非同期処理Tips

しかし、一つのメソッドや処理につき一つのDispatchers、というわけでもなく場合によっては同じ非同期スコープ内でもDispacthersの切りかえを行うことも必要です。

その際は、KotlinのwithContextで同じ非同期スコープ内でDispatchersを変えるだけ、を実現できます。

viewModelScope(Dispatchers.IO) {
    // ... データ取得など
    withContext(Dispatchers.Main) {
        // UI操作など
    }
}

非同期の結果を待つ場合

非同期処理を並列させて、どこかで合流させたい時があります。

そういったときは、launchの戻り値であるJobを保持しておくか、asyncawaitの組み合わせを使いましょう。

suspend fun load() {
    val job1 = viewModelScope.launch(Dispatchers.IO) { 
        // ... 重い処理1
    }
    val job2 = viewModelScope.launch(Dispatchers.IO) {
        // ... 重い処理2
    }
    
    job1.join()
    job2.join()
    
    // ... 続きの処理
}

asyncは結果を戻り値で欲しい時に使います。

また、途中でjob1.cancel()のように途中で実行中の非同期ジョブを停止させることも可能です。

その他参考になりそうなサンプルコード

以下のコードはKotlin コルーチンでアプリのパフォーマンスを改善する | Android developersから関連するものを抜粋しています。(コメントは変更しています)

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }
suspend fun fetchTwoDocs() =        // いすれかのDispatchersで呼ぶ
    coroutineScope {
        val deferreds = listOf(     // 同時にDocsをフェッチ
            async { fetchDoc(1) },  // 1つ目のDocを非同期的に取得
            async { fetchDoc(2) }   // 2つ目のDocを非同期的に取得
        )
        deferreds.awaitAll()        // awaitAllで両方のDocsを待つ
    }

どちらも並列に実行された非同期処理をあるタイミングで待つコードです。

参考ドキュメント

  1. Android での Kotlin コルーチン | Android developers
  2. Android でのコルーチンに関するベスト プラクティス | Android developers
  3. Kotlin コルーチンでアプリのパフォーマンスを改善する | Android developers