【Android / Java】双方向 DataBinding + Clickイベント / サンプルアプリ実装

# はじめに
実務案件で DataBinding を利用している。
なかなか難しい。そこで双方向 DataBinding を用いたサンプルアプリを作成しながら DataBinding について学んだ。

※ 今回は Java ですが、 ~~Kotlin でも同様のものを作成してみようと思っております。その時は改めて投稿します~~。
Kotlin バージョンも作成して記事にしましたのでご覧いただければ幸いです！！↓↓
[【Android / Kotlin】双方向 DataBinding + Clickイベント / サンプルアプリ実装](https://qiita.com/tkmd35/items/516283b4e43fd5ff7a7c)


## 双方向データバインディングとは（一方向との違い）

- 一方行データバインディング
  - ViewModel などの中でデータを変更 → View に反映
- 双方向データバインディング
  - ViewModel などの中でデータを変更 → View に反映
  - ユーザが View でデータを変更 → ViewModel にデータの変更が反映（通知）

一方向が ViewModel などのロジックファイルから View 側への通知だけであるのに対し、
双方向ではユーザがアプリのフォームなどに値を入力したときなどに、ロジックファイルの方にも値の入力（変更）が通知される。と言うものである。

なお一方向データバインディングについては[こちら](https://qiita.com/tkmd35/items/f5cc228328f60ed363ad)で記事にしております。

※ ここで説明している ViewModel とはデータバインディングのロジックを定義してあるファイルのことを指しています。


### サンプルアプリの概要
こんな感じのもの↓↓
無茶苦茶画質悪くてすみません。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621162/bcff8a8b-c834-198c-3a41-9e57bc602535.gif" width="320" />

実装していることとしては

- フォーム入力時
  - 入力されたテキストをフォーム下部にリアルタイムで表示
  - ボタンアクティブにする（テキストの有無で切り替え）
- ボタンクリック時
  - フォーム入力テキストを上部に表示
  - フォームおよび下部のテキストを空欄に戻す（初期化する）


## 開発環境
- Android Studio: 4.1.1
- Build #AI-201.8743.12.41.6953283, built on November 5, 2020
- Runtime version: 1.8.0_242-release-1644-b3-6915495 x86_64
- VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
- macOS: 10.15.7

# 実装

## DataBinding導入
``build.gradle``に記述を追加して DataBindingを利用できるようにする。

```app/build.gradle
plugins {
    id 'com.android.application'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.android.bidirectionaldatabindingsample"
        minSdkVersion 24
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    // ここに記述を追加
    dataBinding {
        enabled = true
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
```

## レイアウト要素をセット
まずは各要素には DataBinding の記述無しの状態で xml にのレイアウトを作成。

※ ``<layout></layout>``だけは先に記述。
このようにレイアウトのルート要素に ``<layout>``でくくったレイアウトファイルがあると、自動的にxmlファイル名に応じたBindingクラスが作られる。
今回: activity_main.xml => ActivityMainBinding

```activity_main.xml
<?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"
    >

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <TextView
            android:id="@+id/click_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.15"
            />

        <EditText
            android:id="@+id/input_form"
            android:layout_width="184dp"
            android:layout_height="wrap_content"
            android:hint="テキストを入力"
            android:inputType="text"
            android:textSize="20dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/button"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.3"
            />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            app:layout_constraintStart_toEndOf="@id/input_form"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/input_form"
            />

        <TextView
            android:id="@+id/realTimeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            app:layout_constraintStart_toStartOf="@id/input_form"
            app:layout_constraintTop_toBottomOf="@id/input_form"
            android:layout_marginTop="16dp"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

## DataBinding のロジック定義をするViewModelを作成する

```ViewModel.java
// BaseObservableクラスを継承
public class ViewModel extends BaseObservable {
// ここにロジックを書いていく
}
```

## MainActivityでViewModelをバインドする（紐づける）

```MainActivity.java
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setTitle("双方向データバインディングサンプル");

        // Bindingインスタンスを生成
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        // ViewModelオブジェクトとバインドする（紐付ける）
        ViewModel viewModel = new ViewModel();
        binding.setViewModel(viewModel);
    }
}
```

## レイアウトファイルでViewModelオブジェクトを利用できるようにする
レイアウトファイルに記述を追加

```activity_main.xml
<?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"
    >

    <data>

    <!--        viewModelという名前でViewModelオブジェクトを登録  -->
        <variable
            name="viewModel"
            type="com.android.bidirectionaldatabindingsample.ViewModel"
            />

    </data>
    
<!--   ・・・ 省略 ・・・   -->
```

準備が整ったのでこれから機能を実装していく。

## フォーム入力でボタンアクティブにする

### ViewModel
以下を記述

- ``formText``
- ``getFormText()``
- ``setFormtext()``
- ``isButtonEnable()``

```ViewModel.java
public class ViewModel extends BaseObservable {
    // 代入せずにnullでも良いが今回は "" を初期値とする。
    private String formText = "";

　　 // フォーム（EditText）の入力値の getter
    @Bindable public String getFormText() {
        return formText;
    }

    // フォーム（EditText）の入力値の setter
    public void setFormText(String formText) {
        this.formText = formText;
        // この記述でisButtonEnable()が呼ばれる
        notifyPropertyChanged(BR.buttonEnable);
    }

    // フォーム（EditText）へのテキスト入力有無で、ボタン活性・非活性を制御するフラグの getter
    @Bindable public boolean isButtonEnable() {
　　　　　// 入力あり:true  入力なし：false
        return !TextUtils.isEmpty(formText);
    }
}
```

### レイアウト
以下を記述

- フォーム要素に``android:text="@={viewModel.formText}"``
- ボタン要素に``android:enabled="@{viewModel.buttonEnable}"``


```activity_main.xml
<!--  ・・・ 省略 ・・・　-->

        <!--  「@={}」で双方向のバインディング -->
        <!--  ViewModelの getFormText() と setFormText() に対応 -->
        <EditText
            android:id="@+id/input_form"
            android:layout_width="184dp"
            android:layout_height="wrap_content"
            android:hint="テキストを入力"
            android:inputType="text"
            android:text="@={viewModel.formText}" ←ここを追加
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/button"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.3"
            />

        <!-- ViewModelの isButtonEnable に対応 -->
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            android:enabled="@{viewModel.buttonEnable}" ←ここを追加
            app:layout_constraintStart_toEndOf="@id/input_form"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/input_form"
            />

<!--  ・・・ 省略 ・・・　-->
```

### 処理の流れ
1. View側でフォームの値を変更（入力または削除）したときに``ViewModel``の``setFormText()``が呼ばれる。
2. このタイミング（``formText``が変更されたタイミング）で``notifyPropertyChanged(BR.buttonEnable)``により``isButtonEnable()``を呼ぶ。
3. ``isButtonEnable()``で``formText``の値があるかどうかにより``boolean``を返す
4. View側``android:enabled="@{viewModel.buttonEnable}"``に``boolean``が入ってくることによりボタンの活性非活性が反映される

### ポイント
``@={viewModel.formText}``により双方向データバインディングが実現される。つまりView側でフォームの値を変更（入力または削除）したときに``ViewModel``の``setFormText()``が呼ばれるようになる。
``@=``ではなく``@``だと一方向のデータバインディングとなり、フォーム入力内容の変更が検知されず``setFormText()``が呼ばれないため注意。


## フォーム入力内容をリアルタイム表示させる
### ViewModel
以下を追加

- ``setFormText()``内に``notifyPropertyChanged(BR.realTimeText);``
- ``getRealTimeText()``

```ViewModel.java
public class ViewModel extends BaseObservable {
    // 代入せずにnullでも良いが今回は "" を初期値とする。
    private String formText = "";

　　 // フォーム（EditText）の入力値の getter
    @Bindable public String getFormText() {
        return formText;
    }

    // フォーム（EditText）の入力値の setter
    public void setFormText(String formText) {
        this.formText = formText;
        // この記述でgetRealTimeText()が呼ばれる
        notifyPropertyChanged(BR.realTimeText);
        // この記述でisButtonEnable()が呼ばれる
        notifyPropertyChanged(BR.buttonEnable);
    }

    // フォーム（EditText）下にリアルタイムで表示するテキスト（TextView）の getter
    @Bindable public String getRealTimeText() { 
        // フォーム入力されているテキストを代入して返す
        String realTimeText = formText;
        return realTimeText;
    }

    // フォーム（EditText）へのテキスト入力有無で、ボタン活性・非活性を制御するフラグの getter
    @Bindable public boolean isButtonEnable() {
　　　　　// 入力あり:true  入力なし：false
        return !TextUtils.isEmpty(formText);
    }
}
```

### レイアウト
フォーム入力の内容をリアルタイムで表示したいTextViewに``android:text="@{viewModel.realTimeText}"``を記述

```activity_main.xml
        <!-- 一部抜粋して記述 -->

        <!-- ViewModelのgetRealTimeText()に対応 -->
        <TextView
            android:id="@+id/realTimeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.realTimeText}"  ←ここを追加
            android:textSize="20dp"
            app:layout_constraintStart_toStartOf="@id/input_form"
            app:layout_constraintTop_toBottomOf="@id/input_form"
            android:layout_marginTop="16dp"
            />
```

### 処理の流れ
1. フォーム入力内容の変更で``setFormText()``内の``notifyPropertyChanged(BR.realTimeText);``により``getRealTimeText()``が呼ばれる。
2. ``getRealTimeText()``の返り値がレイアウトの`android:text="@{viewModel.realTimeText}"``に取得され表示される。

### ポイント
``notifyPropertyChanged(BR.~)``によりプロパティの変更を通知できる。
``getter``に``@Bindable``を付与する。そうすることでモジュールパッケージ内の``BRクラス``にデータバインディングで使用するリソースIDを持つ監視変数が作成されるため、このような記述が可能になる。

詳しくは[こちら](https://developer.android.com/topic/libraries/data-binding/observability?hl=ja)を参照

#### BRクラスへ監視変数作成例
以下二つを記述

- ``@Bindable public String getFormText()``
- ``@Bindable public boolean isButtonEnable()``

```BR.java

public class BR {

  public static final int formText = 1;

  public static final int buttonEnable = 2;
}
```

## ボタンクリックで テキスト表示＋フォーム初期化 をさせる
### ViewModel
以下を追加

- 変数``clickText``
- ``getClickText()``
- ``onButtonClick()``

```ViewModel.java
public class ViewModel extends BaseObservable {
    // 代入せずにnullでも良いが今回は "" を初期値とする。
    private String formText = "";
    // 初期値をセット
    private String clickText = "ボタンクリックでここに表示";

　　 // フォーム（EditText）の入力値の getter
    @Bindable public String getFormText() {
        return formText;
    }

    // ボタンクリック時に表示するテキスト（TextView）の getter
    @Bindable public String getClickText() {
        return clickText;
    }

    // フォーム（EditText）の入力値の setter
    public void setFormText(String formText) {
        this.formText = formText;
        // この記述でgetRealTimeText()が呼ばれる
        notifyPropertyChanged(BR.realTimeText);
        // この記述でisButtonEnable()が呼ばれる
        notifyPropertyChanged(BR.buttonEnable);
    }

    // フォーム（EditText）下にリアルタイムで表示するテキスト（TextView）の getter
    @Bindable public String getRealTimeText() { 
        // フォーム入力されているテキストを代入して返す
        String realTimeText = formText;
        return realTimeText;
    }

    // フォーム（EditText）へのテキスト入力有無で、ボタン活性・非活性を制御するフラグの getter
    @Bindable public boolean isButtonEnable() {
　　　　　// 入力あり:true  入力なし：false
        return !TextUtils.isEmpty(formText);
    }

    // ボタンクリックイベント
    public void onButtonClick() {
        // clickTextにフォーム入力テキストにセット
        clickText = formText;
        // formTextは初期化
        formText = "";
        // 変更を通知する
        // この記述でgetClickText()が呼ばれる
        notifyPropertyChanged(BR.clickText);
        // この記述でgetFormText()が呼ばれる
        notifyPropertyChanged(BR.formText);
    }
}
```

### レイアウト
以下を追加

- ボタンクリック時表示したいテキストに``android:text="@{viewModel.clickText}"  ``
- ボタンに``android:onClick="@{() -> viewModel.onButtonClick()}"``

```activity_main.xml
        <!-- クリック時表示テキストを抜粋 -->

        <!-- ViewModelのgetClickText()に対応 -->
        <TextView
            android:id="@+id/click_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:text="@{viewModel.clickText}"  ←ここを追加
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.15"
            />

        <!-- ボタン部分を抜粋 -->

        <!-- ViewModelのonButtonClick()に対応 -->
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            android:enabled="@{viewModel.buttonEnable}"
            android:onClick="@{() -> viewModel.onButtonClick()}" ←ここを追加
            app:layout_constraintStart_toEndOf="@id/input_form"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/input_form"
            />       

```

### ポイント
ボタンクリックしたときの処理の流れ

1. ``onButtonClick()``が呼ばれる
2. ``clickText``にそのときのフォーム入力内容を代入
3. ``formText``に``""``を代入して初期化
4. ``notifyPropertyChanged(BR.clickText/formText)``でそれぞれの変数の変更をView側に通知
5. ``getClickText()``と``getRealTimeText()``が呼ばれ、 View側に変数の値が反映される

## 補足
Viewが描画される？（アクティビティとバインディングされる？）タイミングでも全ての``getter``が呼ばれていた。それによりView側に変数の値が反映される。

## レイアウトファイル全体

```activity_main.xml
<?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"
    >

    <!--        viewModelという名前でViewModelオブジェクトを登録  -->
    <data>
        <variable
            name="viewModel"
            type="com.android.bidirectionaldatabindingkotlin.ViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <TextView
            android:id="@+id/click_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:text="@{viewModel.clickText}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.15"
            />

        <EditText
            android:id="@+id/input_form"
            android:layout_width="184dp"
            android:layout_height="wrap_content"
            android:hint="テキストを入力"
            android:inputType="text"
            android:textSize="20dp"
            android:text="@={viewModel.formText}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/button"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.3"
            />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            android:enabled="@{viewModel.buttonEnable}"
            android:onClick="@{() -> viewModel.onButtonClick()}"
            app:layout_constraintStart_toEndOf="@id/input_form"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/input_form"
            />

        <TextView
            android:id="@+id/realTimeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:text="@{viewModel.realTimeText}"
            app:layout_constraintStart_toStartOf="@id/input_form"
            app:layout_constraintTop_toBottomOf="@id/input_form"
            android:layout_marginTop="16dp"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```


# 参考
https://developer.android.com/topic/libraries/data-binding/observability?hl=ja

# 最後に
わかりやすく書こうと頑張ってかきました。しかし、かえって冗長でわかりにくくなってしまっていたらすみません。
ご指摘やこうした方が良いなどなどありましたら気軽にコメントいただければ嬉しいです！

DataBinding やっと慣れてきたかも...

冒頭にも記載しましたが、~~Kotlin でも同様のサンプルを作成しようと思っております。~~
Kotlin バージョンも作成して記事にしました！！よろしければ↓↓
[【Android / Kotlin】双方向 DataBinding + Clickイベント / サンプルアプリ実装](https://qiita.com/tkmd35/items/516283b4e43fd5ff7a7c)