概要
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に反映されます。
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
は一度しか呼ばれていないことがわかります。
どう実装されているか?
AOSPのTransformations.javaのコードを見てみます。distinctUntilChanged
はLiveDataを引数に取り、LiveDataを返すstaticメソッドです。previousValue
とcurrentValue
を比較して、異なる値である場合に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
便利なので積極的に使っていこうと思います!