69
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-27

はじめに

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 は解説不要かと思いますが変数の初期値になります。

liveData1liveData2もそのままです。データソースとなるLiveData2つを引数にとります。

blockで高階関数を用いています。高階関数についてはこちらこちらの記事が参考になります。 block<T>liveData1liveData2を引数にとり <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のアプリには参考になる処理がいっぱい入っていますので、時間がある時に頑張って読み解いていきたいと思います。
おかしなところがありましたらマサカリお待ちしています。

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

69
42
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?