distinctUntilChanged を使ったらテストが上手くいかなかった


このテストは通る

class SampleViewModel : ViewModel() {

    private val _liveData = MutableLiveData<Boolean>()
    val liveData: LiveData<Boolean>
        get() = _liveData

    fun updateLiveData(bool: Boolean) {
        _liveData.value = bool
    }
}
class SampleViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Test
    fun testUpdateLiveData() {

        val viewModel = SampleViewModel()

        assertThat(viewModel.liveData.value).isNull()

        viewModel.updateLiveData(true)

        assertThat(viewModel.liveData.value).isTrue()
    }
}


しかし、liveDataをdistinctUntilChangedを用いるように修正すると、assertThat(viewModel.liveData.value).isTrue()のところで通らなくなる

class SampleViewModel : ViewModel() {

    private val _liveData = MutableLiveData<Boolean>()
    val liveData: LiveData<Boolean>
        get() = _liveData.distinctUntilChanged() // 修正

    fun updateLiveData(bool: Boolean) {
        _liveData.value = bool
    }
}

原因 1

distinctUntilChangedの実装を見てみると、MediatorLiveDataを新たに生成し、ソースを追加して、LiveDataとして返している。
つまり、先のコードではliveDataを取得しようとする度に、別のLiveDataが返ってきていた。
また、ソースのLiveData(_liveData)の値に関わらず、初期値がnullのMediatorLiveDataが生成されているので、先のテストでは常にviewModel.liveData.value == nullとなる

Transformations.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;
    }

    ...

対応

ゲッターを用いず、フィールドに保持する。

class SampleViewModel : ViewModel() {

    private val _liveData = MutableLiveData<Boolean>()
    val liveData: LiveData<Boolean> = _liveData.distinctUntilChanged() // 修正

    fun updateLiveData(bool: Boolean) {
        _liveData.value = bool
    }
}

原因 2

addSourceの実装を見てみると、最後にhasActiveObserversでMediatorLiveDataがアクティブなObserverを持っているかどうかを確認し、持っている場合のみplugを呼び出している。

MediatorLiveData.java
    ...

    @MainThread
    public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
        Source<S> e = new Source<>(source, onChanged);
        Source<?> existing = mSources.putIfAbsent(source, e);
        if (existing != null && existing.mObserver != onChanged) {
            throw new IllegalArgumentException(
                    "This source was already added with the different observer");
        }
        if (existing != null) {
            return;
        }
        if (hasActiveObservers()) {
            e.plug();
        }
    }

    ...

plugの実装を見てみると、ソースのLiveDataをobserveしている。
つまり、MediatorLiveData(liveData)が、アクティブなObserverによってobserveされていない場合、ソースのLiveData(_liveData)の値の変更を受け取らない実装になっている。
そのため、先のコードではliveDataの値は更新されず、初期値であるnullのままとなる。

MediatorLiveData.java
    ...

    private static class Source<V> implements Observer<V> {
        final LiveData<V> mLiveData;
        final Observer<? super V> mObserver;
        int mVersion = START_VERSION;

        Source(LiveData<V> liveData, final Observer<? super V> observer) {
            mLiveData = liveData;
            mObserver = observer;
        }

        void plug() {
            mLiveData.observeForever(this);
        }

        void unplug() {
            mLiveData.removeObserver(this);
        }

        @Override
        public void onChanged(@Nullable V v) {
            if (mVersion != mLiveData.getVersion()) {
                mVersion = mLiveData.getVersion();
                mObserver.onChanged(v);
            }
        }
    }

    ...

対応

テスト内でliveDataobserveする。

class SampleViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Test
    fun testUpdateLiveData() {

        val viewModel = SampleViewModel()
        viewModel.liveData.observeForever {} // 追加

        assertThat(viewModel.liveData.value).isNull()

        viewModel.updateLiveData(true)

        assertThat(viewModel.liveData.value).isTrue()
    }
}

まとめ

MutableLiveDataを用いる時に、ゲッターを利用することもあると思うが、distinctUntilChangedを使う場合は、フィールドで保持した方が良い。

また、distinctUntilChangedmapswitchMapなど、MediatorLiveDataを用いる時は、observeしないと値が更新されない。