こんにちは、グノシー Android アプリの開発担当の Liang です。
今回はグノシー Android アプリの開発において、Kotlin Coroutines を用いた実用的な Tips をいくつか紹介したいと思います。

ExoPlayer + Coroutines
ExoPlayerにはLooperを設置出来る、指定したProcessの低い Priority で再生などを実行すれば、Main Thread に行うUIの更新と操作を優先させるCoroutineContextにはLooperを持った Thread も対応しており、asCoroutineDispatcherすることで、Coroutine にも相互の運用が出来る- 改善点:グノシーの運用では、
RecyclerView内に自動再生の動画ViewHolderを実装した場面で、スクロールの操作が更にスムーズになった - 注意点:
ExoPlayerに関する処理は全て非同期になり、設置したLooperを持った Thread で実行しなければならない
val priority = Process.THREAD_PRIORITY_BACKGROUND val handlerThread = HandlerThread("ExoPlayer", priority) val playerHandler = Handler(handlerThread.looper) val exoPlayer = ExoPlayer.Builder().setLooper(playerHandler.looper) val playerContext = playerHandler.asCoroutineDispatcher("PlayerDispatcher") // play by coroutine playerContext.launch { exoPlayer.playWhenReady = true }
- Reference exoplayer.dev kotlinlang.org
Dispatchers.Main + Yield
Dispatchers.Mainで実行した Coroutine の Main Thread 内には、処理を順番に行う決まりになっている- 処理毎に優先度を調整したい時、
yield()を宣言した Coroutine には、該当 Thread を一時的に他の処理に実行を譲ることが出来る yield()後の処理は一時的に中断され、同一 Thread が解放してから、該当 Coroutine を再開する
coroutineScope.launch(Dispatchers.Main) {
println("low priority work launched")
yield()
// The work can be done lately
println("low priority work done")
}
coroutineScope.launch(Dispatchers.Main) {
println("high priority work launched")
// The work must be done right now
println("high priority work done")
}
- 上記の処理結果により、
Low Priorityの Coroutine が後回しにされ、High priorityの Coroutine を完了してから実行することになった
Low priority work launched High priority work launched High priority work done Low priority work done
- 改善点:グノシーの運用では、アプリの起動時に Main Thread を使ったログの送信を後回しにして、フレーム描画によるフリーズが緩和された
- 注意点:
yield()の処理コストは低くないため、使う場面の選定が重要になる - Reference kotlinlang.org
CoroutineDispatcher.limitedParallelism
- 該当
CoroutineContextを用いた並行で実行する Coroutine の数を制限出来る newFixedThreadPoolContextによる特定 Thread Pool の利用とは違い、今空いている Thread を使うため、処理コストが低い- 改善点:グノシーの運用では、記事の複数
ViewHolderを表示すると同時に、記事内容のキャッシュ保存に当たって、並列処理を最多2件に制限することで、実行の負担を減らした - 注意点:必ずしも特定の Thread を使うことではない、
delayやasyncで一時的に suspend した間は他の Coroutine にも実行する - 現時点は
ExperimentalCoroutinesApi
val coroutineContext = Dispatchers.IO.limitedParallelism(2) coroutineScope.launch(coroutineContext) { // doing simultaneously } coroutineScope.launch(coroutineContext) { // doing simultaneously } coroutineScope.launch(coroutineContext) { // waiting to do until the parallelized work done }
- Reference kotlinlang.org
Flow
flatMapConcat
flatMapConcatに変換したFlowでは、次の要素が入る時に、今の Coroutine 結果を出力されてから、次の Coroutine を開始する- 改善点:グノシーの運用では、ユーザーの連続クリックによる重複の処理を防ぐ
- 注意点:
mapにflattenConcatを足す形として新しいFlow内に順次に処理するため、公式によりmapだけの利用も検討して良いでしょう - 現時点は
ExperimentalCoroutinesApi
setOnClickListener {
clickFlow.tryEmit()
}
clickFlow.flatMapConcat {
flow {
// Clicking until the previous clicked event is done
}
}
flatMapLatest
flatMapLatestに変換したFlowでは、次の要素が入る時に、直ちに今の Coroutine をキャンセルし、次の Coroutine を開始する- 改善点:グノシーの運用では、RecyclerView
OnScrollListener.onScrolledの処理が頻繁に呼ばれるため、すぐ上書きされる要素を破棄することで、無駄な処理を減らした - 注意点:Coroutine がキャンセルされていたことにより、
Flow内にCancellationExceptionが発生するため、Exception の制御も必要になる - 現時点は
ExperimentalCoroutinesApi
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { scrollFlow.tryEmit() } scrollFlow.flatMapLatest { flow { // Scrolling be canceled if there is a next scrolled event } }
- Reference kotlinlang.org kotlinlang.org
scan / runningFold
Flow内に一度出力された要素を保存し、関数を実行する度に、全ての要素を順番に返却するrunningReduceとは違い、初期値の設置に別の値として出力出来る- 改善点:グノシーの運用では、
RecyclerView.Adapterの要素の更新に当たって、歴史と最新の要素の比較:DiffResultを出力するまで、一連の処理をStateFlow内で完結出来た - 注意点:Thread の切り替え、Buffer との統合運用にも留意しましょう
itemsFlow
.scan(emptyList<Item>() to DiffResult())
{ (oldItems, diffResult), newItems ->
newItems to DiffUtil.calculateDiff() // with the oldItems
}
.flowOn(Dispatchers.IO)
.collect { (items, diffResult) ->
// notify the adapter's items
}
- Reference kotlinlang.org
感想
Kotlin Coroutines を使う場面が多くなる Android 開発においては、パフォーマンスには少々疑問を思う所もあるかもしれませんが、グノシーの運用としては開発効率の向上を実感しました。今後 Android との親和性も更に高まっていくことでしょう。