RxJavaからCoroutinesへの移行

こんにちは。アプリ開発グループの嶋田です。ニコニコ漫画、読書メーターのAndroidアプリ開発を担当しています。今期よりニコニコ漫画ではRxJavaからCoroutinesへの移行を積極的に進めています。この記事では、移行に至った背景から導入までを紹介していきます。

背景

CoroutinesにはRxJavaに対して既知のメリット*1が存在しますが、それらに加えて次の3点が移行を決めた大きな理由です。

  • 将来性(Jetpackとの統合)
  • ニコニコ漫画が純kotlinプロジェクト
  • 非同期処理の統一

Android開発におけるCoroutinesの将来性はRxJavaに対して明確な強みと言えます。徐々にCoroutinesをサポートしたライブラリが提供され始めています。特にgoogleはKotlin firstを宣言したこともあってか積極的に展開しており、Jetpackライブラリの多くにはCoroutinesを全面的にサポートする拡張機能が用意されています。これらを利用することで生産性の向上が見込めます。

2つ目の理由としてニコニコ漫画が99.9% *2Kotlinのコードで実装されているという点もあります。javaコードが残っていたならそもそもCoroutines化の選択肢は無かったかもしれません。

languages share

最後の理由として非同期処理を統一したかったという背景があります。ニコニコ漫画では非同期処理をRxJava(2 & 3)、Coroutinesを利用して実現しています。これらは複雑性を高める一因となっていたので解消したかったことも理由の1つです。

移行計画

移行にあたっては次の方針を決めました。

  • ワンショット処理のみを対象とする
  • コンバーター(kotlinx-coroutines-rx等)は利用しない

まずはワンショット処理、具体的にはAPIリクエスト等の処理(呼び出されるたびに 1 回実行され、結果が利用可能になるとすぐに完了する処理)のみを移行することにしました。これはワンショット処理のみであれば、ストリーミング処理(Observable = Flow)を考慮する必要が無くなり学習コストを低く抑えられるためです。

コンバーターを使用しないのは単純にパフォーマンス的な理由と、実際にいくつかのAPIを移行してみた結果、そこまでコストをかけずに実現できたため現状はこの方針としています。

導入

導入する上で最も参考になるのは公式サイトの情報です。ただ結構な物量であり、一部粒度が粗く若干わかりにくい箇所も存在します。ここでは自身への備忘録も兼ねてそういった自分にとって理解し辛かった点を紹介していきます。

以降は2022/04/22時点での内容となります。

Jetpack ViewModelの導入

ニコニコ漫画はMVVMパターンで構築しており、ViewModelについてはこれまで独自実装したものを利用していました。Jetpack版ViewModelを導入した際に得られる利益(fragment間連携等)は既存構造の中で実現できており、またCoroutinesのライフサイクル管理についても多少の追加実装でカバーできる範囲であった為、導入*3には消極的でした。ですが、様々なシーンでJetpack版ViewModelであることで生産性が向上するケースが出てきており*4、このタイミングで導入することにしました。

ViewModelの生成ですが、Web上にはandroid.arch.lifecycle:viewmodel.ViewModelProviderやandroidx.lifecycle.ViewModelProvidersを利用した方法が多数紹介されています。これらは既にdeprecatedであり、現在はandroidx.lifecycle.ViewModelProviderを利用するのが正しい方法のようです*5。公式ドキュメントにはこの辺はっきりとしたことが記載されておらず、サンプルコード中にしれっとKTXの拡張メソッドであるviewModels()を使用した方法が見つかるのみです。 ニコニコ漫画ではviewModels()を参考に次のようなinlineを用意しています。

inline fun <reified VM : ViewModel, A1> ComponentActivity.viewModels(factoryFunction: KFunction1<A1, VM>, arg1: A1): VM {

    val factory = object : Factory {
        override fun <VM : ViewModel?> create(modelClass: Class<VM>): VM = factoryFunction(arg1) as VM
    }

    return ViewModelProvider(viewModelStore, factory).get(VM::class.java)
}

固定引数であるのはコンパイルでの型チェックを効かせる事によるメリットが大きいと判断した結果です。

APIインターフェース定義

Http client libとしてRetrofitを利用しています。バージョン2.5以前はCoroutinesに対応しておらず利用するにはadapterが必要でした。2.6にてcoroutinesがサポートされたことによって特別な対応無しにsuspend関数を扱えるようになっています。

Before

@GET(“xxx”)
fun getXXX(): Single<XXX>

After

@GET(“xxx”)
suspend fun getXXX(): XXX

Dispacherの指定

まず使用するDispacherは単体テストを考慮してDI*6から挿入したものを使用するように実装しています。公式的にもDispacherのハードコードは非推奨とされています。

module
@Module
class DispatcherModule {

    @DefaultDispatcher
    @Provides
    fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default

    @IoDispatcher
    @Provides
    fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

    @MainDispatcher
    @Provides
    fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
annotation
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DefaultDispatcher
repository
class EnvironmentRepository @Inject constructor(
    private val remoteDataSource: EnvironmentDataSource,
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher

データ層へのリクエストにおいてはIoDispatcherを利用しています。RxJavaではTHREAD_POOL_EXECUTORを利用していましたが、そもそもAPI30からdeprecatedとなっている、また公式からも用途に応じたスレッドを利用することが推奨されているため変更しています。

最後に

Coroutinesリリース当初はまだまだ機能も不足しておりRxJavaからの移行は現実的ではありませんでした。 そんなCoroutinesも今では成熟し、RxJavaと比べて劣っている点はないと思います。 将来性という点においてはCoroutinesに分があると思われますので、 ニコニコ漫画のように段階的に移行を進めていくのが現時点での最適なアプローチかもしれません。

*1:シンプルなAPI、バックプレッシャーの取扱等

*2:0.1%は設定ファイル等の影響

*3:統合ですかね

*4:compose等

*5:クラスリファレンスを参照していき最終的にdeprecatedでない関数

*6:Hiltはまだstable版がリリースされてから日も浅いためdaggerを使用しています