こんにちは、Reactive人間こと id:yutailang0119 です。
世間にはiOSアプリケーションエンジニアとして認識されていると思いますが、仕事ではAndroidアプリも作っています。
最近は公私で、ReactiveX、ReactiveCocoa、Combine、そしてLiveDataと広義のFunctional Reactive Programming (以下、FRP) と触れ合って生活しています。
RxからCombineの変換にやっと慣れてきたところだけど、LiveDataまで手を出して破滅した
— Yutaro Muta (@yutailang0119) 2019年10月16日
そんな中で、今回はLiveDataネタです。
LiveDataとは
一言で表すなら、「Androidのライフサイクルに寄り添ったFRP Framework」といったところでしょうか。
詳細な説明は、LiveData Overview やその他の詳細な解説ブログたち任せます。
LiveDataの欠点
ReactiveX/RxJavaやReactiveX/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 }
ただ、 name
に Non-Null
な値だけを流すのは、 map
だけでは一筋縄にいかなそうです。
Transformations
突然出てきた Transformations
ですが、android.arch.lifecycle.Transformationsに実装されているOperatorまとめクラスです。
ドキュメントには map
、switchMap
のみしか記載がありませんが、Version 2.1.0で distinctUntilChanged
も実装されました。
ただ、やはりReactiveXに慣れてしまった我々には、Operatorが足りない...
ということで、LiveDataを拡張して、Operatorを追加しましょう。
LiveDataにOperatorを追加する
前提
ここでは拡張したLiveDataをKotlinからのみ使用する場合を想定しています。
Javaの世界は考慮できていませんので、ご了承を。
Transformationsの実装を参考にする
Transformations.distinctUntilChangedの挙動を確かめる - Qiita にAndroid Open Source Projectの実装が載っているので、参考にしてみます。
/** * 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()
filter
と notNull
を実装したことで、 name
に Non-Null
な値だけを流せるようになりました。
解説
LiveDataの拡張関数として定義しています。
前述の通り、 Transformations
はJavaで実装されているため、 Transformation
のCompanion objectの拡張関数としては実装できなそうです。
なので、LiveDataを直接拡張しました。
Transformations.map
と書き方と違うので、ちょっともやっとしますね。
そこで androidx.lifecycle:lifecycle-livedata-ktx
を使用しましょう。
Version 2.1.0 でTransformationsのOperatorの拡張関数が実装されています。
Added ktx extensions for
LiveData.observe
methods andTransformations.*
methods.
使い方をさらに変更
これが顧客が本当に欲しかったものだ!!!
val user: MutableLiveData<User> = MutableLiveData(user) val name: LiveData<String> = user .map { it?.name } .filter { !it.isNullOrEmpty() } .notNull()
まとめ
今回の要領で、MediatorLiveData
で拡張関数を実装すれば、ReactiveXの他のOperatorたちも実装できそうですね。
Transformations
自体に filter
や notNull
くらいは実装してくれよと思うし、その内に追加されるのではとも思っています。
「LiveDataはそういうもんじゃないんだよ」と言われるかもしれませんが、直感的に書けることは真だと思っているので、ぜひお試しください。
「こういうのあるともっと便利だぜ」等ありましたら、 @yutailang0119までどうぞ!