Transformations.distinctUntilChangedの挙動を確かめる

# 概要

12月17日の[AndroidXのリリース](https://developer.android.com/jetpack/androidx/androidx-rn#2018-dec-17)で[androidx.lifecycleも2.1.0-alpha01がリリースされ](https://developer.android.com/jetpack/androidx/androidx-rn#2018-dec-17-lifecycle)、その中に「`Transformations.distinctUntilChanged`が追加されたよ」と書かれていたので気になったで試してみました。

# インストール

バージョンには`2.1.0-alpha01`を指定します。

```gradle
dependencies {
    // Android Jetpack Architecture components
    def lifecycleVersion = '2.1.0-alpha01'
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
    kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
}
```

# 実装

簡単なサンプルアプリを実装しました。フォームに入力してボタンをクリックすると、上部のTextViewに反映されます。

![12月-21-2018 18-14-02.gif](https://qiita-image-store.s3.amazonaws.com/0/43438/7e4b3ff7-f3b3-de05-36c3-f3f2b2b61108.gif)

ViewModelでは`Transformations.distinctUntilChanged`によって変換したLiveDataを公開しています。

```kotlin

class MainActivityViewModel : ViewModel() {

    private val _value: MutableLiveData<Int> = MutableLiveData()
    val value: LiveData<Int> = Transformations.distinctUntilChanged(_value)

    fun setValue(value: Int) {
        _value.postValue(value)
    }
}
```

ActivityではViewModelで公開されたvalueをDatabindingを使用してTextViewに紐づけています。また、ボタンがクリックされた際に`setValue`メソッド経由で入力された値をViewModelにセットしています。

```kotlin
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        val viewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
        binding.setLifecycleOwner(this)
        binding.viewModel = viewModel
        binding.button.setOnClickListener {
            viewModel.setValue(binding.editText.text.toString().toInt())
        }
    }
}
```

レイアウトは以下の通りです。

```xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable name="viewModel"
            type="com.horie1024.distinctuntilchangedsample.MainActivityViewModel"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/output_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:textSize="24sp"
            android:text="@{viewModel.value.toString()}"
            app:layout_constraintBottom_toTopOf="@+id/edit_text"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_chainStyle="packed" />

        <EditText
            android:id="@+id/edit_text"
            android:layout_width="240dp"
            android:layout_height="wrap_content"
            android:inputType="number"
            app:layout_constraintBottom_toTopOf="@+id/button"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/output_text" />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="CLICK"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/edit_text" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

# テスト

テストコードを書いて確認してみます。

```kotlin
@RunWith(AndroidJUnit4::class)
class MainActivityViewModelTest {

    @Rule
    @JvmField
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Mock
    lateinit var observer: Observer<Int>

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
    }

    @Test
    fun distinctUntilChangedの挙動確認() {

        val viewModel = MainActivityViewModel()

        viewModel.value.observeForever(observer)
        viewModel.setValue(1)
        viewModel.setValue(1)

        verify(observer, times(1)).onChanged(1)
    }
}
```

このテストコードは無事成功し、`setValue`が複数回呼ばれても`Observer.onChanged`は一度しか呼ばれていないことがわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43438/bec8f760-4e6f-178a-b4cc-662fe2c0776c.png)


# どう実装されているか？

[AOSPのTransformations.javaのコード](https://android-review.googlesource.com/c/platform/frameworks/support/+/763608/5/lifecycle/livedata/src/main/java/androidx/lifecycle/Transformations.java)を見てみます。`distinctUntilChanged`はLiveDataを引数に取り、LiveDataを返すstaticメソッドです。`previousValue`と`currentValue`を比較して、異なる値である場合に`outputLiveData`に値をセットしています。

```java

@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;
}
```

# サンプルコード

こちらで公開しています。

https://github.com/horie1024/DistinctUntilChangedSample

# まとめ

`Transformations.distinctUntilChanged`便利なので積極的に使っていこうと思います！