Android-Binding の Binding の実装を深掘りしてみる

完全に自分用メモなので乱文です。

MVVM とは

Model-View-ViewModel なアーキテクチャ。 View(画面)は、ViewModel のプロパティ(Android-binding では IObservable)の変更を検知し、コントロール(Android ではウィジェット)の内容を置き換える。(OneWay の場合)

TowWay の場合、ユーザーの操作によってコントロールの値が変化した時、ViewModel のプロパティにその変更を適用する。

超断片的なので、詳しくはおググりください。

例: EditText の Text と、ViewModel の Address プロパティが関連付け(バインド)されていた場合、

  • ViewModel.Address がプログラム処理によって変更された時に、EditText.Text が自動的に変更されるのが OneWay。
  • 上記に加え、EditText にユーザーが文字を入力した時に、ViewModel.Address にその値が適用されるのが、TwoWay。

Android-Binding とは

Android で MVVM できるライブラリ。

何が知りたいのか?

TwoWay の時、EditText にユーザーが文字列を入力して、ViewModel.Address に新しい値を設定する。すると、その変更をまた View が検知して、EditText の値を書き換える処理が動いてしまう(OneWay 側の動きをする)のではないか?

通常の文字列などでは、EditText に同じ値が再設定されるだけなので問題なさげだが、それでも意図しないUI更新は気持ち悪い。

調べてみる

Android-Binding では、この辺りを上手く処理しているように見えるので、実装を見てみる。

Android-Binding で、ViewModel 側のプロパティを "Property"、それをバインドする View 側の定義を "Attribute" という。というか勝手にそう呼ぶことにする。(実体はどちらも IObservable の実装だが、この辺を説明しだすと混乱するので割愛) 前述の例だと、「EditText の Text」が "Attribute"、ViewModel.Address が "Property" になる。

  1. Attribute に Property がバインドされると、Attribute#onBind が呼び出され、この中で Attribute#Bridge クラス(これは Observer である)が生成され、Bridge が Property の変更を監視する。と同時に this.subscribe(mBridge) にて TextViewAttribute 自身の変更も監視する。[github]

  2. [TextViewAttribute]ユーザーが EditText の Text を変更すると、TextViewAttribute#onTextChanged が呼び出され、さらに notifyChanged が呼び出される。[github]

  3. [TextViewAttribute]notifyChanged() は Attribute の基底クラスである Observable#notifyChanged() が呼び出され、続いてオーバーロードである notifyChanged(initiators) が呼ばれる。 [github]

  4. [TextViewAttribute]initiators とは、.NET イベントの sender のようなもので、イベント(コールバック)発生元のオブジェクトを示す。ただし、Observable#notifyChanged() の時点では、initiators はまだ空っぽ。

  5. [TextViewAttribute]notifyChanged(Collection<Object> initiators) にて、initiators に this つまり TextViewAttribute 自身を追加して、監視している Observer の onPropertyChanged を呼び出す。[github]

  6. [TextViewAttribute]TextViewAttribute の監視者は Attribute#Bridge クラスであり、Bridge#onPropertyChanged(prop, initiators) が呼び出される。この時点で、prop は TextViewAttribute 自身、initiators にも TextViewAttribute 自身が格納されている。[github]

  7. [TextViewAttribute]Bridge#onPropertyChanged の最初の if文 if (prop==mAttribute){ が true となり、mBindedObservable._setObject(prop.get(), initiators); が呼び出される。mAttribute は TextViewAttribute 自身であり、mBindedObservable はバインドした ViewModel の Property である。[github]

  8. [Property]呼び出されたのは、ViewModel の Property(Observable)、つまり Observable#_setObject(newValue, initiators) である。initiators には依然として TextViewAttbute が1件格納されている。ここから Observable#set(newValue, initiators) が呼び出される。[github]

  9. [Property]Observable#set(newValue, initiators) では、doSetValue で、自身(ViewModel の Property)の値を書き換え、その後、initiators に this、つまり ViewModel の Property を追加して notifyChanged を呼び出す。この時点で initiators は 2件(TextViewAttribute と ViewModelのProperty) である。[github]

  10. [Property]notifyChanged(Collection<Object> initiators) にて、initiators に更に this を追加して(この処理に意味があるのかは謎)、監視者である Observer の onPropertyChanged(this, initiators) を呼び出す。この時点で initiators は 3件(TextViewAttribute と ViewModelのProperty と ViewModelのProperty) [github]

  11. [TextViewAttribute]再び Bridge#onPropertyChanged(prop, initiators) が呼び出される。ここでは prop は ViewModelのProperty である。これにより、最初の if文 は false となる。[github]

  12. [TextViewAttribute]次の if文 else if (prop==mBindedObservable){true となるが、更にその次の if文 if (initiators.contains(Attribute.this)) は、initiators に TextViewAttribute が含まれている為 false となり、処理は終わる。これにより、次の行にある mAttribute._setObject(prop.get(), initiators); は呼び出されない、つまり、EditText の Text に値が再設定されることはない。[github]

まとめ

  • Binder の実装は、Bridge が EditText と ViewModelのProperty 両方の変更を監視し、また initiators という仕組みをうまく使うことで、EdtiText が起こした Property の変更は、Attribute が検知しないように工夫されていた。

Android-Binding の仕組みはなんとかわかったー。.NET の XAML はどうやってんのかな?