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を決めれば良いということです。
しかし、一つのメソッドや処理につき一つのDispatchers、というわけでもなく場合によっては同じ非同期スコープ内でもDispacthersの切りかえを行うことも必要です。
その際は、KotlinのwithContext
で同じ非同期スコープ内でDispatchersを変えるだけ、を実現できます。
viewModelScope(Dispatchers.IO) {
// ... データ取得など
withContext(Dispatchers.Main) {
// UI操作など
}
}
非同期の結果を待つ場合
非同期処理を並列させて、どこかで合流させたい時があります。
そういったときは、launch
の戻り値であるJob
を保持しておくか、async
とawait
の組み合わせを使いましょう。
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を待つ
}
どちらも並列に実行された非同期処理をあるタイミングで待つコードです。