概要
Android Jetpack の以下の2つのクラスを同時に使えなくてもやもやする人はちょこちょこいるんじゃないかと思います。
- Data Binding の BaseObservable クラス
- ViewModel の ViewModel クラス
なぜ同時に使えないかというとどちらも継承して使うタイプのクラスだからです。あっちを継承すればこっちが継承できず、こっちを継承すればあっちが継承できない…。
私は Data Binding を以前から使っていて、最近になって ViewModel を覚えようとしているという流れの中にいます。同じ流れにいる人で同じもやもやに遭遇している人は他にもいるのではないでしょうか?
全く別のところが作っているライブラリが群がぶつかるならわかるけど Android Jetpack という1つのグループの兄弟に位置づけられているようなものもはずなのになんで…というもやもやです。
もやもやを解消すべく色々なところの情報を探したり、実際に手を動かして試行錯誤したりしました。
その試行錯誤のうち中の失敗談「ViewModel に BaseObservable を合成しようとした話」をご紹介します。同じ過ちを犯さないようにという意味で参考にしてください!
本記事は Kotlin を前提としています。二つのクラスを合成するのに Kotlin の言語機能を使うからです。
ViewModel に BaseObservable を合成しよう。
Kotlin には委譲という機能があります。インターフェイスの実装を別のインスタンスに横流しして処理してもらうという機能です。
BaseObservable クラスは、Observer インターフェイスの簡易実装なので ViewModel を継承したクラスに BaseObservable のプロパティを追加して Observable インターフェイスの実装をそのプロパティのインスタンスに委譲すれば簡単に合成できるじゃないか!という発想です。
具体的な実装を以下に示します。
package example;
import android.arch.lifecycle.ViewModel;
import android.databinding.BaseObservable;
import android.databinding.Observable;
class XxxViewModel(val baseObservable:BaseObservable = BaseObservable())
: ViewModel(), Observable by baseObservable {
// 後は好きな実装をどうぞ!
}
「Observable by baseObservable」の部分が移譲している部分です。簡単ですね!
コンパイルも普通に通りますし、実行時エラーも出ません!これは行けたな!(確信)
でもちゃんと動かない。…その原因とは?
ViewModel としての機能は動きました。
しかし、BaseObservable としての機能である notifyPropertyChanged に View が反応しませんでした…。
例えば以下のようなクラスを作って、
package example;
import android.arch.lifecycle.ViewModel;
import android.databinding.BaseObservable;
import android.databinding.Observable;
class MainActivityViewModel(val baseObservable:BaseObservable = BaseObservable()) : ViewModel(), Observable by baseObservable {
var textValue1: String
@Bindable
get
set(value)
field = value
baseObservable.notifyPropertyChanged(BR.textValue1)
}
バインドします。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<data>
<variable name="vm" type="example.MainActivityViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- ここに入力した値を双方向バインドですぐ下にテキスト表示する -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={vm.textValue1}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{vm.textValue1}" />
</LinearLayout>
</layout>
何がいけないのでしょう?
内部のソースコードを追っていってわかりました。
なんと View 側の実装に「プロパティ所持者と変更通知主体のインスタンス一致確認」処理が入っていたんです!
参考までに具体的に以下のソースコードの部分です。通知の受け取りが遮断されています。
public void onPropertyChanged(Observable sender, int propertyId) {
// ...
if (obj != sender) {
return; // notification from the wrong object?
}
// ...
}
Observable の実装を別のインスタンスに委譲したため、今回紹介した方法はプロパティ所持者と変更通知主体が当然一致しません。
周辺のソースを読みましたが、回避する方法はなさそうなので、この方法はダメそうですね!(白目)
で、どうするの?
BaseObservable と ViewModel は簡単には同時に使えないと思うのでもやもやするけど諦めましょう。
私は BaseObservable / Bindable / ObservableField の使用を止めて ViewModel / LiveData / MutableLiveData に置き換える方向で現行のソースを書き替えています。
状況によって他にも方針はあるでしょう。
ViewModel を使わないという方向に進むのも(別に今までなくてもやってこれていたわけだから)ありだと思います。