個人事業主になったので真面目にアウトプットしてみるブログ

フリーで顧客の会社に潜り込んでAndroidアプリつくって、とりあえず食っていけるようにアウトプットを増やしていくブログです。

Android界隈でKotlin Coroutinesの登場によってRxJavaが不要と言われる原因とそれに対する反論

ちまたでKotlin Coroutinesの登場でRxJavaが不要になりつつあるという噂が流れているみたいなので、どう言う状況なのかまとめてみました。

さっき書いたこの記事

pooh3-mobi.hatenablog.jp

まぁ論拠は薄いし、そんなデータそろえてって、みんながすげー納得するようなことを一朝一夕でやるなんて、データサイエンティストさんに失礼ですよ。マナー違反ですね。 ということで、とりあえず前提としてRxJavaは通信ライブラリであるRetrofitとセットで広がったものでリアクティブプログラミングをやろうと思って使っていた人はほとんどいなかった!(衝撃的事実)という前提で話を展開していくとします。

Android界隈でKotlin Coroutines以外に最近あった出来事ってなんでしょうか? それは・・・まぁ大きいので言えばAndroid Jetpackですよね。 そしてその中にあって一番使えてそうなのがAndroid Architecture Components(以下AAC)周りなんじゃないでしょうか?(しらんけど?)

Kotlinを導入してさえいればRxJavaの部分はKotlin Coroutinesに丸ごと置き換えられる可能性がある

一般的にRxJavaは通信ライブラリのRetrofitとセットで使われていてRPは見向きもされていないって前提での話ですが

AACをつかってAndroidなMVVMの実装としてDataBindingとViewModelとLiveDataを使っていると、非同期処理部分さえどうにかすれば、それだけで完結してしまえそうと言う感触が、正直な話、あります。つまりちゃんとワーカースレッドをつかった重たい処理の部分を並列、直列、リトライ、エラーハンドリング、キャンセル、重複回避、レースコンディション、タスクのキュー管理、画面を跨いだ処理んためのシングルトンなインスタンスの保持、ライフサイクルの管理などをうまくやってしまいさえすれば、後はなんとでもなる感じです。 その状態にさえあれば、正直RetrofitとRxJavaでViewModelまでデータを引っ張ってくる部分は不要です。

ってところで、Kotlin Coroutineの登場ですよ。(デキレースかと勘ぐりますね)

ということは、ここでAACでMVVMを構成しているとRxだった部分がKotlin Coroutinesに置き換えることが可能になりそうって話なんだと思います。

じゃぁそれ以外はどうなんだって話があるし、非DataBindinでMVVMなものを実装してViewとのBindingをうまくやるって話も考えると、RxBindingは必要そう。 MVPもViewとBinding自体をしなくなるのでKotlinでやっているのなら非同期処理周りはKotlin Coroutinesでもいけそうですね?View側からのEditTextの入力イベントやDialogの応答もだいたいラムダで書ける+Delegateで最小限の実装でコールバックを書いたりしやすいので、そこそこ問題はなさそうです。

もちろんKotlinがまだ導入できていない企業があり、まだKotlinでいいのか?って攻めあぐねているような状況のところもあるでしょうし、Kotlinにしたくて上司を説得したり、スケジュールとKPIとにらめっこしてそれ以上進めなくなっているところもあるでしょう。そういうところではRxJavaすら入っていない可能性も割と濃くあるでしょうが、まぁそもそも論でいうと、Kotlin Coroutinesの話なんて全くもってカスってこない状況だと思うので、そこは目を閉じておくとすると、まぁまぁな範囲でRxJavaからKotlin Coroutinesに移行が可能でしょう。

これ以上は言うことないですね。特別なRxを必要とするアーキテクチャを採用していないかぎり、ほとんどのケースではKotlin CoroutinesにするともしかしたらRxJavaを使うより保守性の高いコードが書けるかもって期待があるんですよねきっと。(僕はしらんけど)

ここまでで、一切RxJavaが不要と言われる部分の反論してないっすね・・・ははは。

あえて反論する

そもそもRxってなんなのかって言うとFRPの実装の一つとして考えられるます。(広義では)

pooh3-mobi.hatenablog.jp

FRPっていうのは関数型が持っている高階関数をつかったデータフローの特徴をもった宣言的なすたいるのプログラミングパラダイムの一つで、いくつか実装方法があってRxもその一つと考えても良いと。

Forms · An Introduction to Elm

次に、これはElmというフロントエンドに特化した言語のコードですが、これもRxで言うところの全てのイベントとそれ以外のObservableのmergeをつかったイベントの統合と状態変更、変更状態をUIに伝えるscanをつかったRxのコードに置き換え可能です。これに近いアーキテクチャとして知られるのがReduxというアーキテクチャらしいのですが僕は正味つかったことはないですが、フロントエンドでは割と使われるようなので、似たようなノウハウをもってAndroidに組み込んでくるとい感じなんでしょうかしりませんけど。

でもまぁそういうアーキテクチャ部分でもRxが活躍する可能性があるわけです。Elmと言う言語は途中までFRPを実装した言語として進んでいたけれど難しい(何が難しかったかはしらないけど)ので路線変えたらしいのですが、みると普通にElmアーキテクチャ適用したFRPだとおもいましたけど、どこらへんがFRPじゃないんでしょうか。頭が悪いのでわかりません。

まぁFRPやElmの影響を受けて書いてみた下のコードをみていただけばわかるように、わりとRxもAACと同梱しても良い感じになる可能性がViewModel側ではまだあると思っています。

class MyViewModel : ViewModel() {

    val api: () -> ApiResult by lazy { { ApiResult.Success.also { Thread.sleep(3000) } } }

    // ユーザからのイベントを通知
    val msg = behaviorProcessor<Msg>()

    // ユーザからのイベント処理結果を観測する
    val submitEnable: LiveData<Boolean>
    // TODO 再Bindに備えて一度しかデータを通知しないLiveData実装に置き換えが必要
    val submitResult: LiveData<Result>
    val isLoading: LiveData<Boolean>

    init {
        // input port to output port
        // ※ここでRxからLiveDataへのデータフローの変換を宣言(ここらへんRxだけにして自動生成でなんとかする手もありそうですが?)
        val outputs = create(LongInputs(event = msg))
        submitEnable = outputs.submitEnable.toLiveData()
        submitResult = outputs.submitResult.toLiveData()
        isLoading = outputs.isLoading.toLiveData()
    }

    fun create(inputs: MyInputs): MyOutputs {

         // ※Rxを使ってイベントを合成

        val event = inputs.event
        // 非同期処理があるのでobserveOnしておく
        val state = event.observeOn(Schedulers.io())
            .update<Pair<Model, Cmd>, Msg>(
                initialValue = Model.Nothing to Cmd.NONE,
                accumulator = { _, msg ->
                    when (msg) {
                        is Msg.Submit -> Model.Loading to Cmd.FETCH_DATA
                        is Msg.GotResult -> msg.result to Cmd.NONE
                    }
                }
            )

        // updateで更新されたModelとCmdから次のアクションを定義しMsgとしてEventに伝える
        // 非同期処理 Success/Failureが返ってくる(今回はSuccess固定)
        val fetchData: Pair<Cmd, () -> Msg> = Cmd.FETCH_DATA to { Msg.GotResult(api()) }
        state.setCommand(fetchData, event)

        return MyOutputs( // LiveDataに渡す前にデータを加工。LiveDataは基本DataBindingのつなぎとして利用
            submitEnable = state.map { it.first != Model.Loading },
            submitResult = state.map { it.first }.ofType(ApiResult::class.java),
            isLoading = state.map { it.first == Model.Loading }
        )
    }
}

private fun Flowable<Pair<Model, Cmd>>.setCommand(pair: Pair<Cmd, () -> Msg>, event: BehaviorProcessor<Msg>) {
    this.filter { it.second == pair.first }.map { pair.second() }.subscribe(event)
}

fun <T: Any> Publisher<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this)

fun <R, T> Flowable<T>.update(initialValue: R, accumulator: (R, T) ->R): Flowable<R> = this.scan(initialValue, accumulator)

つまりLiveDataでは表現仕切れない部分や、RxJavaが得意な部分をうまくかみ合わせることで、十分AACに組み込んで価値を発揮していけると僕は思います。たしかに非同期処理部分はKotlin CoroutinesかRxJavaどちらかにしないと一貫性が出ないので、メンテコストが増大する。そして大抵の処理だとRxJavaよりKotlin Coroutinesを利用したほうがコードが完結になる可能性が高い(ホントか?)ので、その部分に関しては置き換えが進むのかもしれませんが、RxJavaの真価はそもそも厳密に表示的意味論(数式みたいなやつかな?しらんけど)にそぐわない問題(Rxは複数のStreamからシグナルがほぼ同時に来た時に、最新の状態を一度で表現しきれず、古い状態を一度送る: zip(0,0)となってほぼ同時にzip(1,1)となっても通知は[1,0], [1, 1]の順に通知されるみたいな問題があるらしい、それが起きないことを前提にして作っていて、その問題に遭遇したら厄介そう。一発で同時に押さないとエラーになるゲームとか? )があるにしろFRPパラダイムの影響を色濃く受けている部分があるわけなのでゲームやノンブロッキングでネットワーク上のノードとリアルタイムでやりとりしてデータの整合性を保つみたいな神秘にあふれたシステムを作るわけでもないと思うし、設計図を書いたら勝手にRxに変換してコード生成してくれる機械学習を使った装置があるわけでもないので、そこまで心配することなくFRPっぽい実装にRxを使っても良いんじゃないですかねって言う感じで僕の主張は終わります。

まぁほんと、FRPとRxのギャップで致命的になる問題って、よくわかってないので、具体的な問題がわかる人がいたらコメントくれたらめっちゃ勉強になりそうなのでよろしくお願いします!