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

# はじめに
[こちら](https://qiita.com/tkmd35/items/ed2ae410e875aba165f7)の記事にて Java で同様のサンプルアプリを作成している。
今回はその Kotlin バージョンである。

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

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

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

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

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

### サンプルアプリの概要
こんな感じのもの↓↓
無茶苦茶画質悪くてすみません。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/621162/1e287549-5647-dd71-360f-36f7568be286.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を利用できるようにする。
2箇所に記述。
Java と異なり、アノテーションを導入する記述 (kapt) が必要になるので注意が必要。
→ [こちら](https://qiita.com/tkmd35/items/d9876ade4e43c8bf1be3)で kapt導入について の記事も書いてます。

```app/build.gradle
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt' // ←ここに追加
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.android.bidirectionaldatabindingkotlin"
        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'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

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

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    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.kt
// BaseObservableクラスを継承
class ViewModel : BaseObservable() {
// ここにロジックを書いていく
}
```

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

```MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        title = "双方向データバインディング Kotlin"

        // ViewModel とバインディングする（紐付ける）
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = 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.bidirectionaldatabindingkotlin.ViewModel" />
            />

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

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

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

### ViewModel
以下を記述

- ``@get:Bindable ~ set(value)``
- ``isButtonEnable()``

```ViewModel.kt
class ViewModel : BaseObservable() {

    // これでフォーム入力内容(formText)の getterとsetter が同時にセットできる
    @get:Bindable
    var formText: String = ""
        set(value) {
            // fieldはformTextのこと
            // フォーム入力内容がformTextにセットされる
            field = value
            // View側にbuttonEnableの変更を通知(isButtonEnable()を呼ぶ)
            notifyPropertyChanged(BR.buttonEnable)
        }

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

#### 補足
Java に 比べると Kotlin は getter と setter を両方定義する際かなりシンプルに記述できる。

```ViewModel.kt

    @get:Bindable
        var formText: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.buttonEnable)
            }
```

↑↑ は ↓↓ こう書いたのと同じである。

```ViewModel.kt
    private var formText = ""

    @Bindable fun getFormText(): String {
        return formText
    }

    fun setFormText(formText: String) {
        this.formText = formText
        notifyPropertyChanged(BR.buttonEnable)
    }

```

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

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


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

        <!--  「@={}」で双方向のバインディング -->
        <!--  ViewModelの formText定義 @get:Bindable~set(value) に対応 -->
        <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``の``formText``の``set()``処理が呼ばれる。
2. このタイミング（``formText``が変更されたタイミング）で``notifyPropertyChanged(BR.buttonEnable)``により``isButtonEnable()``を呼ぶ。
3. ``isButtonEnable()``で``formText``の値があるかどうかにより``boolean``を返す
4. View側``android:enabled="@{viewModel.buttonEnable}"``に``boolean``が入ってくることによりボタンの活性非活性が反映される

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


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

- ``formText``の``set()``内に``notifyPropertyChanged(BR.realTimeText);``
- ``getRealTimeText()``

```ViewModel.kt
class ViewModel : BaseObservable() {

    // これでフォーム入力内容(formText)の getterとsetter が同時にセットできる
    @get:Bindable
    var formText: String = ""
        set(value) {
            // fieldはformTextのこと
            // フォーム入力内容がformTextにセットされる
            field = value
            // View側にrealTimeTextの変更を通知(getRealTimeText()を呼ぶ)
            notifyPropertyChanged(BR.realTimeText)
            // View側にbuttonEnableの変更を通知(isButtonEnable()を呼ぶ)
            notifyPropertyChanged(BR.buttonEnable)
        }

    // フォーム入力内容をフォーム下のTextViewに反映する getter
    @Bindable fun getRealTimeText(): String {
        // return formText でも良いがわかりやすく?一旦変数に代入してから return している
        val realTimeText = formText
        return realTimeText
    }

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

### レイアウト
フォーム入力の内容をリアルタイムで表示したい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. フォーム入力内容の変更で``formText``の``set()``内の``notifyPropertyChanged(BR.realTimeText);``により``getRealTimeText()``が呼ばれる。
2. ``getRealTimeText()``の返り値がレイアウトの`android:text="@{viewModel.realTimeText}"``に取得され表示される。

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

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

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

- ``@get:Bindable var formText ~ set(value)``
- ``@Bindable fun isButtonEnable(): Boolean``

```BR.java

public class BR {

  public static final int formText = 1;

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

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

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

```ViewModel.kt
class ViewModel : BaseObservable() {
    private var clickText: String = "ボタンクリックでここに表示"

    // これでフォーム入力内容(formText)の getterとsetter が同時にセットできる
    @get:Bindable
    var formText: String = ""
        set(value) {
            // fieldはformTextのこと
            // フォーム入力内容がformTextにセットされる
            field = value
            // View側にrealTimeTextの変更を通知(getRealTimeText()を呼ぶ)
            notifyPropertyChanged(BR.realTimeText)
            // View側にbuttonEnableの変更を通知(isButtonEnable()を呼ぶ)
            notifyPropertyChanged(BR.buttonEnable)
        }

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

     // フォーム入力内容をフォーム下のTextViewに反映する getter
     @Bindable fun getRealTimeText(): String {
         // return formText でも良いがわかりやすく?一旦変数に代入してから return している
         val realTimeText = formText
         return realTimeText
     }

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

    // ボタンクリックイベント
    fun 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://qiita.com/takaaki7/items/91d34e8bf9ad5d71ddd2
- https://developer.android.com/topic/libraries/data-binding/observability?hl=ja


# 最後に
データバインディングの書き方は他にも``ObservableField()``を利用したものなど他にも方法があるようですが、今回はこのような方法で書きました。

今後も Android開発極めるべく、勉強していきます！！

ありがとうございました！！