search
LoginSignup
8

More than 3 years have passed since last update.

posted at

updated at

Organization

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

サンプルコード

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

まとめ

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

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
What you can do with signing up
8