Help us understand the problem. What is going on with this article?

Transformations.distinctUntilChangedの挙動を確かめる

概要

12月17日のAndroidXのリリースandroidx.lifecycleも2.1.0-alpha01がリリースされ、その中に「Transformations.distinctUntilChangedが追加されたよ」と書かれていたので気になったで試してみました。

インストール

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

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

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

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にセットしています。

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

テスト

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

@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

どう実装されているか?

AOSPのTransformations.javaのコードを見てみます。distinctUntilChangedはLiveDataを引数に取り、LiveDataを返すstaticメソッドです。previousValuecurrentValueを比較して、異なる値である場合にoutputLiveDataに値をセットしています。

@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便利なので積極的に使っていこうと思います!

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away