Kotlin LiveDataにReactiveXっぽいOperatorを追加する

こんにちは、Reactive人間こと id:yutailang0119 です。

世間にはiOSアプリケーションエンジニアとして認識されていると思いますが、仕事ではAndroidアプリも作っています。
最近は公私で、ReactiveXReactiveCocoaCombine、そしてLiveDataと広義のFunctional Reactive Programming (以下、FRP) と触れ合って生活しています。

そんな中で、今回はLiveDataネタです。

LiveDataとは

一言で表すなら、「Androidのライフサイクルに寄り添ったFRP Framework」といったところでしょうか。
詳細な説明は、LiveData Overview やその他の詳細な解説ブログたち任せます。

LiveDataの欠点

ReactiveX/RxJavaReactiveX/RxSwiftを使ってきた人が、LiveDataに触れた時の戸惑いは、Operatorの少なさだと思います。
ReactiveXにはこんなにも豊富なOperatorが用意されています。

一方、素のLiveDataでは、 map 一つで苦労します。

LiveDataの map

val user: MutableLiveData<User?> = MutableLiveData() // RXSwiftでいうとBehaviorRelayだと考えてもらえば、ここではよい
val name: LiveData<String?> = Transformations.map(user) { it?.name }

ただ、 nameNon-Null な値だけを流すのは、 map だけでは一筋縄にいかなそうです。

Transformations

突然出てきた Transformations ですが、android.arch.lifecycle.Transformationsに実装されているOperatorまとめクラスです。
ドキュメントには mapswitchMapのみしか記載がありませんが、Version 2.1.0distinctUntilChanged も実装されました。

ただ、やはりReactiveXに慣れてしまった我々には、Operatorが足りない...

ということで、LiveDataを拡張して、Operatorを追加しましょう。

LiveDataにOperatorを追加する

前提

ここでは拡張したLiveDataをKotlinからのみ使用する場合を想定しています。
Javaの世界は考慮できていませんので、ご了承を。

Transformationsの実装を参考にする

Transformations.distinctUntilChangedの挙動を確かめる - QiitaAndroid Open Source Projectの実装が載っているので、参考にしてみます。

https://android-review.googlesource.com/c/platform/frameworks/support/+/763608/5/lifecycle/livedata/src/main/java/androidx/lifecycle/Transformations.java

    /**
     * Creates a new {@link LiveData} object does not emit a value until the source LiveData value
     * has been changed.  The value is considered changed if {@code equals()} yields {@code false}.
     *
     * @param source the input {@link LiveData}
     * @param <X>    the generic type parameter of {@code source}
     * @return       a new {@link LiveData} of type {@code X}
     */
    @MainThread
    @NonNull
    public static <X> LiveData<X> distinctUntilChanged(@NonNull LiveData<X> source) {
        final MediatorLiveData<X> outputLiveData = new MediatorLiveData<>();
        outputLiveData.addSource(source, new Observer<X>() {

            boolean mFirstTime = true;

            @Override
            public void onChanged(X currentValue) {
                final X previousValue = outputLiveData.getValue();
                if (mFirstTime
                        || (previousValue == null && currentValue != null)
                        || (previousValue != null && !previousValue.equals(currentValue))) {
                    mFirstTime = false;
                    outputLiveData.setValue(currentValue);
                }
            }
        });
        return outputLiveData;
    }

MediatorLiveData を内部で使って、変更をhookした時にobserve先に値を流すかを判断する処理を隠蔽したLiveDataを作るstaticメソッドとして実装されているようです。
これを参考に MediatorLiveData を利用すれば、内部に処理を隠蔽したOperatorを実装できそうです。

ということで、以下に答えです。

filter

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

fun <T> LiveData<T>.filter(predicate: (T?) -> Boolean): LiveData<T> {
    val mediatorLiveData: MediatorLiveData<T> = MediatorLiveData()
    mediatorLiveData.addSource(this) {
        if (predicate.invoke(it)) {
            mediatorLiveData.value = it
        }
    }
    return mediatorLiveData
}

notNull

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

fun <T> LiveData<T?>.notNull(): LiveData<T> {
    val mediatorLiveData: MediatorLiveData<T> = MediatorLiveData()
    mediatorLiveData.addSource(this) {
        if (it == null) return@addSource
        mediatorLiveData.value = it
    }
    return mediatorLiveData
}

使い方

val user: MutableLiveData<User?> = MutableLiveData()
val name: LiveData<String> = Transformations.map(user) { it?.name }
    .filter { !it.isNullOrEmpty() }
    .notNull()

filternotNull を実装したことで、 nameNon-Null な値だけを流せるようになりました。

解説

LiveDataの拡張関数として定義しています。
前述の通り、 TransformationsJavaで実装されているため、 TransformationCompanion objectの拡張関数としては実装できなそうです。
なので、LiveDataを直接拡張しました。

Transformations.map と書き方と違うので、ちょっともやっとしますね。
そこで androidx.lifecycle:lifecycle-livedata-ktx を使用しましょう。
Version 2.1.0 でTransformationsのOperatorの拡張関数が実装されています。

Added ktx extensions for LiveData.observe methods and Transformations.* methods.

使い方をさらに変更

これが顧客が本当に欲しかったものだ!!!

val user: MutableLiveData<User> = MutableLiveData(user)
val name: LiveData<String> = user
    .map  { it?.name }
    .filter { !it.isNullOrEmpty() }
    .notNull()

まとめ

今回の要領で、MediatorLiveData で拡張関数を実装すれば、ReactiveXの他のOperatorたちも実装できそうですね。
Transformations 自体に filternotNull くらいは実装してくれよと思うし、その内に追加されるのではとも思っています。
「LiveDataはそういうもんじゃないんだよ」と言われるかもしれませんが、直感的に書けることは真だと思っているので、ぜひお試しください。

「こういうのあるともっと便利だぜ」等ありましたら、 @yutailang0119までどうぞ!