はじめに
2020/1/14にDroidKaigi 2020のアプリがGitHubに公開されました。今も盛んに有志によるContributeが行われています。
DroidKaigiのアプリは毎年、その時点でほぼ最新と言える技術を使った実装がふんだんに盛り込まれており、ソースを覗くだけでもとても参考になります。
その中で、個人的に興味を惹かれた処理を紹介します。
注意:DroidKaigiアプリは現在も絶賛アップデート中のため、今後実装が変更になる可能性もあります。
実装紹介
紹介するのは「combine」という関数です。
元ソースは conference-app-2020/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt にあります。
inline fun <T : Any, LIVE1 : Any, LIVE2 : Any> combine(
initialValue: T,
liveData1: LiveData<LIVE1>,
liveData2: LiveData<LIVE2>,
crossinline block: (T, LIVE1, LIVE2) -> T
): LiveData<T> {
return MediatorLiveData<T>().apply {
value = initialValue
listOf(liveData1, liveData2).forEach { liveData ->
addSource(liveData) {
val currentValue = value
val liveData1Value = liveData1.value
val liveData2Value = liveData2.value
if (currentValue != null && liveData1Value != null && liveData2Value != null) {
value = block(currentValue, liveData1Value, liveData2Value)
}
}
}
}.distinctUntilChanged()
}
なにこれ
「なるほど、わからん」となっている方のためにこの関数がどういう処理を行っているか解説しますと、ざっくり言うと 「2つのLiveDataの値を監視し、いずれかの値が変更された時に結果を出力する関数」 になります。
Rxを使った事がある方には combineLatest
をLiveDataで使えるようにした関数、というと分かりやすいかもしれません。
例えばこういう時に使える
ログインID/パスワードを入力するテキストボックスと、ログインボタンがあるとします。
2つのテキストボックスに値が入って初めてボタンが有効になる処理を作りたい場合、上記の combine
を使うと以下のように非常にシンプルに書く事ができます。
val loginId = MutableLiveData<String>()
val password = MutableLiveData<String>()
val isButtonEnabled: LiveData<Boolean> = combine(false, loginId, password) { _, login, pass ->
// ログインIDとパスワードが入力されたらボタンを有効にする
!login.isNullOrEmpty() && !pass.isNullOrEmpty()
}
上記の変数をDataBindingで各コントロールに紐付けてあげれば、簡単にログイン時のバリデーションを実装する事が可能です。
なお、 combine
を実装しない場合は以下のような感じで実装する事になるかと思います。
val loginId = MutableLiveData<String>()
val password = MutableLiveData<String>()
val isButtonEnabled2 = MediatorLiveData<Boolean>().apply {
val observer = Observer<String> {
val login = loginId.value
val pass = password.value
// ログインIDとパスワードが入力されたらボタンを有効にする
value = !login.isNullOrEmpty() && !pass.isNullOrEmpty()
}
// 値を監視する
addSource(loginId, observer)
addSource(password, observer)
}
2つの実装を見比べると、 combine
を使う事でコードの可読性が非常に高くなる事が分かると思います。
実装を読み解く
そんなに難しい事をしているコードではないので、不要かもしれませんが自分なりに実装を解説してみます。
宣言部分
inline fun <T : Any, LIVE1 : Any, LIVE2 : Any> combine(
initialValue: T,
liveData1: LiveData<LIVE1>,
liveData2: LiveData<LIVE2>,
crossinline block: (T, LIVE1, LIVE2) -> T
): LiveData<T> {
initialValue
は解説不要かと思いますが変数の初期値になります。
liveData1
、liveData2
もそのままです。データソースとなるLiveData2つを引数にとります。
block
で高階関数を用いています。高階関数についてはこちらやこちらの記事が参考になります。 block
は<T>
、liveData1
、liveData2
を引数にとり <T>
を返す関数になります。
ここで、block
は関数リテラルによる記述が可能なため、このcombine
関数の呼び出しは
combine(T, liveData1, liveData2) { t, live1, live2 ->
}
と書く事ができます。
※関数をinline fun
で宣言することのメリットは分かっていません…誰かアドバイスお願いします。
続いて関数の中身を見ていきます。
関数の中身
return MediatorLiveData<T>().apply {
value = initialValue
listOf(liveData1, liveData2).forEach { liveData ->
addSource(liveData) {
val currentValue = value
val liveData1Value = liveData1.value
val liveData2Value = liveData2.value
if (currentValue != null && liveData1Value != null && liveData2Value != null) {
value = block(currentValue, liveData1Value, liveData2Value)
}
}
}
}.distinctUntilChanged()
MediatorLiveData
は、あるLiveDataの値を監視(addSource
)して自身の値を変更するLiveDataになります。詳しい解説はこちらにあります。
MediatorLiveData
は通常ひとつのLiveDataに対して用い、複数の値を監視する場合は addSource
を値の数だけ登録しますが、
listOf(liveData1, liveData2).forEach { liveData ->
addSource(liveData) {
...
とする事によって複数のLiveDataに対して一気に値を監視できるようにしています。( listOf
を使うのは目から鱗でした)
if (currentValue != null && liveData1Value != null && liveData2Value != null) {
value = block(currentValue, liveData1Value, liveData2Value)
}
LiveDataの値がnullでない時のみ block
関数を呼び出しています。
block
関数内の結果を直接 value
に渡す事によって、結果的に先ほどの
combine(T, liveData1, liveData2) { t, live1, live2 ->
}
のラムダ式内に記述した処理を元に値が変更される事になります。
}.distinctUntilChanged()
distinctUntilChanged()
は、LiveDataの 値が変更された場合にのみ変更を通知する 機能です。androidx.lifecycle:lifecycle-livedata-ktx
ライブラリに含まれています。詳しい実装はこちらの記事が参考になります。
distinctUntilChanged()
がある事によって、元データが変わった時のみ値の更新処理が走るため、無駄が無くなります。
おわりに
上記以外にもDroidKaigiのアプリには参考になる処理がいっぱい入っていますので、時間がある時に頑張って読み解いていきたいと思います。
おかしなところがありましたらマサカリお待ちしています。
本記事を書くにあたって、以下のリンク先を参考にさせていただきました。