DroidKaigi 2020アプリの「combine」が便利という話

# はじめに
2020/1/14に[DroidKaigi 2020のアプリ](https://github.com/DroidKaigi/conference-app-2020)がGitHubに公開されました。今も盛んに有志によるContributeが行われています。

DroidKaigiのアプリは毎年、その時点でほぼ最新と言える技術を使った実装がふんだんに盛り込まれており、ソースを覗くだけでもとても参考になります。

その中で、個人的に興味を惹かれた処理を紹介します。

*注意：DroidKaigiアプリは現在も絶賛アップデート中のため、今後実装が変更になる可能性もあります。*

# 実装紹介
紹介するのは「combine」という関数です。
元ソースは [conference-app-2020/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt](https://github.com/DroidKaigi/conference-app-2020/blob/master/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2020/ext/LiveDatas.kt) にあります。

```kotlin
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/パスワードを入力するテキストボックスと、ログインボタンがあるとします。

２つのテキストボックスに値が入って初めてボタンが有効になる処理を作りたい場合、上記の `combine` を使うと以下のように非常にシンプルに書く事ができます。

```kotlin
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` を実装しない場合は以下のような感じで実装する事になるかと思います。

```kotlin
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` を使う事でコードの可読性が非常に高くなる事が分かると思います。

# 実装を読み解く

そんなに難しい事をしているコードではないので、不要かもしれませんが自分なりに実装を解説してみます。
## 宣言部分
```kotlin
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`で高階関数を用いています。高階関数については[こちら](https://qiita.com/k5n/items/964d765767a65cc3de5b)や[こちら](https://qiita.com/teruroom/items/a21654c3fa663ee86f48)の記事が参考になります。 `block` は`<T>`、`liveData1`、`liveData2`を引数にとり `<T>` を返す関数になります。

ここで、`block`は[関数リテラル](https://qiita.com/teruroom/items/a21654c3fa663ee86f48)による記述が可能なため、この`combine`関数の呼び出しは

```kotlin
combine(T, liveData1, liveData2) { t, live1, live2 ->

}
```

と書く事ができます。

※関数を`inline fun`で宣言することのメリットは分かっていません…誰かアドバイスお願いします。


続いて関数の中身を見ていきます。

## 関数の中身
```kotlin
    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になります。詳しい解説は[こちら](https://qiita.com/tatsuyakit/items/0abf5684419af61959b4)にあります。

`MediatorLiveData`は通常ひとつのLiveDataに対して用い、複数の値を監視する場合は `addSource` を値の数だけ登録しますが、

```kotlin
listOf(liveData1, liveData2).forEach { liveData ->
    addSource(liveData) {
    ...
```
とする事によって複数のLiveDataに対して一気に値を監視できるようにしています。( `listOf` を使うのは目から鱗でした)

```kotlin
if (currentValue != null && liveData1Value != null && liveData2Value != null) {
    value = block(currentValue, liveData1Value, liveData2Value)
}
```

LiveDataの値がnullでない時のみ `block` 関数を呼び出しています。
`block`関数内の結果を直接 `value` に渡す事によって、結果的に先ほどの

```kotlin
combine(T, liveData1, liveData2) { t, live1, live2 ->

}
```

のラムダ式内に記述した処理を元に値が変更される事になります。


```kotlin
}.distinctUntilChanged()
```

`distinctUntilChanged()` は、LiveDataの **値が変更された場合にのみ変更を通知する** 機能です。`androidx.lifecycle:lifecycle-livedata-ktx`ライブラリに含まれています。詳しい実装は[こちらの記事](https://qiita.com/Horie1024/items/0c1f4a9e83b7ebda98dc)が参考になります。

`distinctUntilChanged()`がある事によって、元データが変わった時のみ値の更新処理が走るため、無駄が無くなります。

# おわりに
上記以外にもDroidKaigiのアプリには参考になる処理がいっぱい入っていますので、時間がある時に頑張って読み解いていきたいと思います。
おかしなところがありましたらマサカリお待ちしています。

本記事を書くにあたって、以下のリンク先を参考にさせていただきました。

- [Kotlin文法 - 関数とラムダ [Qiita]](https://qiita.com/k5n/items/964d765767a65cc3de5b)
- [はじめてのKotlin:高階関数の定義と利用 [Qiita]](https://qiita.com/teruroom/items/a21654c3fa663ee86f48)
- [Transformations.distinctUntilChangedの挙動を確かめる [Qiita]](https://qiita.com/Horie1024/items/0c1f4a9e83b7ebda98dc)
- [MediatorLiveDataとカスタムTransformation [Qiita]](https://qiita.com/tatsuyakit/items/0abf5684419af61959b4)
- [MediatorLiveData で DataBinding を少し楽にする [Qiita]](https://qiita.com/kyanro@github/items/e153246b9d84428441e0)