10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ZOZOテクノロジーズその2Advent Calendar 2018

Day 21

Transformations.distinctUntilChangedの挙動を確かめる

Last updated at Posted at 2018-12-21

概要

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?