独自Viewに双方向DataBinding

欲しいなぁと思っていた機能が簡単に実装可能なのでメモ

---

# はじめに

この記事は独自に実装したViewに対して、
EditText.textへの双方向DataBindingのようなものを実装するための方法を記載します

```
            <EditText
                    ...
                    android:text="@={viewModel.text}" />

```

これを、こうして双方向DataBindingする方法

```
            <jp.honkot.CustomizedEditText
                    ...
                    app:text="@={viewModel.text}" />

```

なお、この例では初歩的なBaseObservableを利用していますが、
ObservableFieldやMutableLiveDataでも代用可能です。

# 前提

- DataBindingの利用方法を心得ている
- 双方向DataBindingの使い方を心得ている

# 実装イメージ

図にするとこんな感じ

<img width="926" alt="Screen Shot 2019-07-22 at 12.41.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/85214/d2b5143c-a7d2-9cf3-7cdb-b7d6b0e84a2b.png">


# 実装

順を追って実装しましょう

## Step.1 独自Viewの実装

割愛しますが、まぁこんな感じで書くと思います

```CustomizedEditText.kt
class CustomEditText @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
    ...
}
```

## Step.2 InverseBindingMethodsアノテーションを指定

独自ViewクラスにInverseBindingMethodsアノテーションを実装します
この`attribute`で指定した属性名がそのままXMLファイル上で双方向DataBindingで指定する属性になります

```CustomizedEditText.kt
@InverseBindingMethods(
    InverseBindingMethod(
        type = CustomEditText::class,  // 対象のクラス
        attribute = "text"  // 作成する属性
    )
)
class CustomEditText @JvmOverloads constructor(
```

## Step.3 Getter/Setterを用意

指定した双方向DataBindingでやりとりするGetter/Setterを指定
なお、ここではCustomizedEditTextの中でもViewModelが存在し、
その中の属性も双方向DataBindingをしていると仮定する

```CustomizedEditText.kt
    fun getText(): String {
        return viewModel.input
    }

    fun setText(value: String) {
        viewModel.input = value
    }

    class ViewModel : BaseObservable() {
        @Bindable
        var input: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.input)
            }
    }
```

## Step.4 InverseBindingListenerを設置

このInverseBindingListenerが、双方向DataBindingで情報をやりとりするインターフェースだと思ってください。
これを独自Viewに持たせる必要があります。この例では独自ViewのViewModelに持たせます

```CustomizedEditText.kt
    fun setInverseBindingListener(listener: InverseBindingListener) {
        viewModel.listener = listener
    }

    class ViewModel : BaseObservable() {
        @Bindable
        var input: String = ""
            set(value) {
                field = value
                listener?.onChange() // ここ、重要
                notifyPropertyChanged(BR.input)
            }

        var listener: InverseBindingListener? = null
    }
```



## Step.5 BindingAdapterを作成

XMLに記載するためのBindingAdapterを作成
このアノテーションで定義することで、指定Viewに対するXML上での拡張属性を作成可能

```CustomizedEditText.kt
    object CustomEditTextBindingAdapter {
        @BindingAdapter("textAttrChanged") // 属性名+AttrChanged
        @JvmStatic
        fun setTextWatcher(view: CustomEditText?, listener: InverseBindingListener) {
            view?.setInverseBindingListener(listener)
        }
    }
```

## Step.6 拡張Viewに対して双方向DataBindingを指定

```MainActivity.kt
    private val binding: ActivityMainBinding by lazy {
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.viewModel = viewModel
    }

    class ViewModel : BaseObservable() {
        @Bindable
        var textWatcher: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.textWatcher)
            }
    }
```

```activity_main.xml
            <jp.honkot.CustomEditText
                    ...
                    app:text="@={viewModel.textWatcher}"/>
```

こうすると、常に独自Viewの値を監視できます。