Introduction of Data Binding in Android

本稿ではData bindingの実装手順を備忘録として残しておきます。

SeekBarとViewModel、TextViewを結びつけて、SeekBarの変化を画面表示に反映する機能を実装します。

使用する主な道具としては、`SeekBar`・`Data Binding`・`LiveData`になります。

[Githubに実装がおいてあります。](https://github.com/sahara-ooga/android-data-binding-sample)説明上コードを適宜省略していますので、詳しくはこちらのレポジトリを参照してください。また、説明と対応するコミットへのリンクを掲載していますので、実装を追記する位置はこちらを参照してください。

## 環境

macOS 10.15.3

Android Studio 3.5.1

Kotlin plugin 1.3.61-release-Studio3.5-1

## 実装手順

以降、package名を example.android.android_data_binding_sampleとして進めます。

### ビルド設定

新規プロジェクトをEmpty Activityを選んで作ったら、まず、app/build.gradleでData Binding用のビルド設定を行います。

```groovy:build.gradle(app)
android {
    ...
    dataBinding {
        enabled = true
    }
}
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/ac77f672130f5acf65199d534999146ec9556d49)

### ViewModel

続いて、ViewModelを実装します。

```kotlin:MainViewModel.kt
class MainViewModel : ViewModel() {
    /**
     * シークバーの値を格納する
     * */
    private val _seekBarValue = MutableLiveData(50)

    /**
     * シークバーの値を表示用に整形した文字列
     * */
    val seekBarValueString: LiveData<String> = Transformations.map(_seekBarValue) {
        "value: $it"
    }

    /**
     * シークバーの値が変化した時に呼び出される
     * */
    fun onSeekBarValueChanged(value: Int) {
        _seekBarValue.postValue(value)
    }
}
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/8e6924d130adb110da4cc9f1879fb6ab1ff3cb76)

MutableLiveDataのイニシャライザなどでビルドエラーになる場合は、app/build.gradleにライブラリの依存関係を追加します。

```groovy:build.gradle(app)
dependencies {
    ...
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    ...
}
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/1adba692fae95ed5d6e5b4ba2477a10cc1246fea)

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

次に、activity_main.xmlを編集します。

`<layout>`タグで既存の記述を囲みます。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <androidx.constraintlayout.widget.ConstraintLayout
        ...
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/1bca0974c6c6d61e7e3e159adc7b7c9ba2809b98)

先程実装したViewModelの宣言を行います。

```xml:activity_main.xml
<layout>

    <data>

        <variable
            name="viewModel"
            type="example.android.android_data_binding_sample.MainViewModel" />
    </data>

...
</layout>
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/7d2e0dd2c943829832ea78408e3778342cf08ff1)

最後に、SeekBarと、SeekBarの値を表示するTextViewを宣言します。ついでにConstraintLayoutからLinearLayoutに変更します。

```xml:activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
...
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical">

        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="20dp"
            android:progress="50" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.seekBarValueString}"
            android:textAlignment="center" />

    </LinearLayout>
    
 </layout>
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/67449821cd856ead4dccac804d827bdd56b5db7b)

ここでのポイントは、TextViewの`text`属性をviewModelの`seeekBarValueString`プロパティに紐づけている箇所です。
 
 ```xml:activity_main.xml
  <TextView
 android:text="@{viewModel.seekBarValueString}"
 />
 ```

TextViewの`text`を直接ViewModelの`LiveData<String>`型のプロパティに紐付けています。

### MainActivity

MainActivityの編集に移ります。

まず、ViewModelのプロパティを追加します。

```kotlin:MainActivitity.kt
class MainActivity : AppCompatActivity() {
    private val viewModel by lazy {
        ViewModelProvider(this).get(MainViewModel::class.java)
    }
...
```

この際、ViewModelProviderのインポートを追加します。

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/68b21bf03d5a25d6e26a5941993ccb7c58038f39)

次に、バインディングオブジェクトのプロパティをMainActivityに追加します。自動生成されるこの型の名称は、対応するレイアウトファイルのファイル名をパスカルケースにし、 Bindingを末尾につけたものになります。

例：レイアウトファイル名が`activity_foo.xml`なら`ActivityFooBinding`になる

```kotlin:MainActivitity.kt
import example.android.android_data_binding_sample.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
...
    private lateinit var binding: ActivityMainBinding
...
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/3c2b5a9feac205ee5ac559d78797d624807e63b6)

続いて、`onCreate()`内に、data bindingの設定を追記していきます。

プロジェクト生成時にデフォルトで書かれる`setContentView()`をDataBindingUtilのそれに差し替え、バインディングクラスをインスタンス化します。

```kotlin:MainActivitity.kt
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
...
```

この際に、`DataBindingUtil`のインポート文も追加します。

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/301faf54310d35d73239a8013a3a5914e6b58d14)

LiveDataオブジェクトをバインディングクラスとともに使用するため、MainActivityをライフサイクル所有者に指定します。

```kotlin:MainActivitity.kt
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this//追記
...
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/b7de39ea4326d5673e2c50304acf9f4b0dd56aed)

続いて、先程定義したMainViewModelをバインディングクラスと共に使用するため、バインディングクラスのプロパティに指定します。

```kotlin:MainActivitity.kt
...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.viewModel = viewModel//追記
...
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/8d016680c375be63db18b1d015e348c26738bdd4)

最後に、SeekBarの変化への対応を設定します。

```kotlin:MainActivitity.kt
...
    override fun onCreate(savedInstanceState: Bundle?) {
...
        binding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                this@MainActivity.viewModel.onSeekBarValueChanged(progress)
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {
            }

            override fun onStopTrackingTouch(seekBar: SeekBar?) {
            }

        })
    }
```

[対応するコミット](https://github.com/sahara-ooga/android-data-binding-sample/commit/5e8357442390d3f3ece65fb2ac058a67a4c6bceb)

これで実行してみます。

シークバーを動かすと、TextViewの値が変わるのが確認できたでしょうか。

![data_binding.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162670/226f645a-4b10-7b98-1f32-de8d719e4b70.gif)


## まとめ

ViewModelの(LiveDataの)プロパティをレイアウトファイル上のTextViewと紐付け、さらにSeekBarの変化をViewModelに伝える設定を行いました。

その結果、SeekBarの変化に応じてTextViewが変化するようになりました。

ユーザーがSeekBarを操作すると、次のようにメソッド呼び出し・プロパティの変化が生じます：

```
(MainActivity)
SeekBar.OnSeekBarChangeListener.onProgressChanged()の呼び出し

(MainViewModel)
viewModel.onSeekBarValueChanged()の呼び出し

_seekBarValue.postValue()の呼び出し

seekBarValueStringのvalueプロパティが更新される

(activity_main.xml)
seekBarValueStringに紐付けられたTextViewのtextプロパティが更新される
```

以上になります。

ここまでお読みいただきありがとうございます。お疲れ様でした。

# 参考

[レイアウト ビューをアーキテクチャ コンポーネントにバインドする | developer.android.com](https://developer.android.com/topic/libraries/data-binding/architecture.html?hl=ja#top_of_page)

[AndroidのデータバインディングとLiveDataを使ってみる | SONICMOOV LAB](https://lab.sonicmoov.com/development/app-dev/android-data-binding/)