LoginSignup
7
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-10

このテストは通る :white_check_mark:

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()のところで通らなくなる :x:

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しないと値が更新されない。

7
2
0

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
7
2