Edited at
AndroidDay 10

MediatorLiveData で DataBinding を少し楽にする

LiveDataを~Bindingに直接Bindできるようになり、色々と開発が捗るようになりました。

しかし、素直にLiveDataを利用するだけだと

「あれ?これもうちょっときれいにかけそうな気がするんだけど、どう書けばいいんだろう・・・」

みたいな問題にぶつかることがあります。

この記事ではそんな時に思い出してほしいMediatorLiveDataについて簡単に説明します。


たまにぶつかる問題点


  • LiveData A と LiveData B の値から、LiveData C の値を作りたい

  • AかBのどちらかが変更されたらCもそのタイミングで変更したい


具体的な例


  • EditText A(prefixText) と EditText B (mainText) を準備

  • TextView C(combinedText) は、"prefixText" + "mainText" の文字列を表示する

  • prefixText または mainText の文字列が変更されたら combinedText に表示する文字列も変更する


layout

    <androidx.constraintlayout.widget.ConstraintLayout

android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>

<EditText
android:id="@+id/prefixText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.prefixText}"
app:layout_constraintTop_toTopOf="parent"
tools:text="prefix text"
/>

<EditText
android:id="@+id/mainText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.mainText}"
app:layout_constraintTop_toBottomOf="@+id/prefixText"
tools:text="main text"
/>

<TextView
android:id="@+id/combinedText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.combinedText}"
android:layout_marginTop="50dp"
android:textSize="30sp"
app:layout_constraintTop_toBottomOf="@+id/mainText"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
tools:text="combined text"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

class MainActivityViewModel {

val prefixText = MutableLiveData<String>().apply { value = "" }
val mainText = MutableLiveData<String>().apply { value = "" }
val combinedText = MutableLiveData<String>().apply { value = "" }
}


素直に書いた例

ViewModelを設定しているActivityのonCreateなどで下記のコードを書く

val textObserver = Observer<String> {

val prefix = viewModel.prefixText.value ?: ""
val main = viewModel.mainText.value ?: ""
viewModel.combinedText.value = "$prefix-$main"
}
viewModel.prefixText.observe(this, textObserver)
viewModel.mainText.observe(this, textObserver)

prefixText, mainText のどちらを変更しても combinedText が変更されて、問題なく動作することが確認できます。

しかし、Activity側でLiveDataをobserveして別のLiveDataに値をセットする、というのはなんとなく気になるところです。

combinedText 自体に、prefixText, mainText を監視して自分自身の値を変更する機能があればいいのに・・・

ご安心ください。

そんな機能が用意されています。

それが今回ご紹介するMediatorLiveDataです。


MediatorLiveDataを利用した例

ActivityのonCreateなどに書いていたコードを削除する

ViewModelを次のように書き換える

class MainActivityViewModel {

val prefixText = MutableLiveData<String>().apply { value = "" }
val mainText = MutableLiveData<String>().apply { value = "" }
val combinedText = MediatorLiveData<String>()

init {
val textObserver = Observer<String> {
val prefix = prefixText.value ?: ""
val main = mainText.value ?: ""
combinedText.value = "$prefix-$main"
}
// prefixText が変更されたら、textObserver の処理が実行されるように設定する
combinedText.addSource(prefixText, textObserver)
// mainText が変更されたら、textObserver の処理が実行されるように設定する
combinedText.addSource(mainText, textObserver)
}
}

Activityに書いてあった処理がなくなり、ViewModel内でやりたいことが完結するようになりました。

Bindのためのコードも、自動生成されるActivityMainBindingからだけになり、バグも減らせそうでいい感じです。


まとめ


  • MediatorLiveDataを利用すると、他のLiveDataの変更のタイミングで自分自身の値の変更を行うことができる

  • DataBindingと合わせて利用した場合、Bindのためのコードが自動生成されるクラスからのみになるため見通しがよくなる

  • これ、RxでいうcombineLatestでは・・・?


おまけ(確認に使ったサンプルアプリの動き)