Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Android + Kotlin Coroutines の実用的な開発Tips

こんにちは、グノシー 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
}

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 を使うことではない、delayasyncで一時的に 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
}

Flow

flatMapConcat

  • flatMapConcatに変換したFlowでは、次の要素が入る時に、今の Coroutine 結果を出力されてから、次の Coroutine を開始する
  • 改善点:グノシーの運用では、ユーザーの連続クリックによる重複の処理を防ぐ
  • 注意点:mapflattenConcatを足す形として新しいFlow内に順次に処理するため、公式によりmapだけの利用も検討して良いでしょう
  • 現時点はExperimentalCoroutinesApi
setOnClickListener {
    clickFlow.tryEmit()
}
clickFlow.flatMapConcat {
    flow {
        // Clicking until the previous clicked event is done
    }
}

flatMapLatest

  • flatMapLatestに変換したFlowでは、次の要素が入る時に、直ちに今の Coroutine をキャンセルし、次の Coroutine を開始する
  • 改善点:グノシーの運用では、RecyclerViewOnScrollListener.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
    }
}

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
    }

感想

Kotlin Coroutines を使う場面が多くなる Android 開発においては、パフォーマンスには少々疑問を思う所もあるかもしれませんが、グノシーの運用としては開発効率の向上を実感しました。今後 Android との親和性も更に高まっていくことでしょう。

参考

www.techyourchance.com