Android - DataBindingはじめ
いまさらAndroidでDataBinding触れはじめてみた浅い記事
Created at Updated at

2640 Words
⚠️

追記 (2020-07-21)

(先月頭だかのAndroidStudio4.0アップデートで起きた話なので、割と今さら感あるが)
データバインディングを使用したいときに必要な設定の記述がAndroid Gradle Plugin 4.0.0では次のものに変わっていた。ちなみにGradleのバージョンは6.1.1だった。

android {
    buildFeatures {
        dataBinding true
    }
}

以前の記述法 でも今のところ動きはするがビルド時に警告が出る。

DSL element 'android.dataBinding.enabled' is obsolete and has been replaced with 'android.buildFeatures.dataBinding'.

追記 (2020-03-31)

lifecycleに関する依存先のバージョンを2.2.0にアップデート。
それに伴い、ViewModelProviderを使用したViewModelのインスタンス生成方法を修正。

ViewModelProviders.of(owner)ViewModelProvider(owner)

追記 (2020-03-10)

BindingAdapterに関するいくつかのこと - すいはんぶろぐ.io

関連する記事としてバインディングアダプタの使い方を書いた。


最近ずっとSatenaのActivity/Fragmentのコード側を作り直す作業をしていて、ViewModel + LiveDataは割と活用してきて"前よりは"いい感じになってきているのだが、
DataBindingに関しては以前UWPアプリ作るとき(中途半端に)触れて以来、MVVMというかデータバインディングやったらやったでそれはそれで色々と面倒なことがあるイメージがあったので積極的に取り入れてなかったのだけど、楽できそうな部分ではやっていこうかと唐突に思った。

今さら感強い上にまだ詳しく使い込んでいないので、
とりあえず導入的なものと、
主に忘れそうな部分についていくつかメモを書く。

導入

以下、Android Studio 4.0Android Gradle Plugin 4.0.0Gradle 6.1.1時点でのお話になります。

build.gradle (app)

ViewModelやらDataBindingを使用するのに必要な設定・依存関係を追加する。

android {
    buildFeatures {
        dataBinding true
    }
}

dependencies {
    ...
    // ViewModel and LiveData
    def lifecycle_version = "2.2.0"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
    testImplementation "androidx.arch.core:core-testing:2.1.0"
    ...
}

activity_hoge.xml

以前のレイアウトの内容を<layout>で囲い、<data> ~ </data>にバインドに必要な情報を記述する。
ここでは、コード側で用意してバインドするViewModelのオブジェクトをレイアウトファイル内ではvmとして扱う。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- bindするデータ -->
    <data>
        <variable
                name="vm"
                type="com.suihan74.hoge.HogeViewModel" />
    </data>

    <!-- 以下表示部分 -->
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/hoge_layout"
        android:background="?attr/panelBackground"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        ...

    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

HogeActivity.kt

ActivityHogeBindingはレイアウトまで書いてビルドすると自動的に生成される。
ViewDataBindingを継承した"アッパーキャメルなレイアウトファイル名 + Binding"という名前のクラスができるので、これをDataBindingUtil.setContentView<T>(...)の型引数に与えて呼ぶとバインドが開始される。

class HogeActivity : AppCompatActivity() {
    private val viewModel: HogeViewModel by lazy {
        // ViewModelの生成
        // ViewModel用のFactoryを用意しておけばViewModelを生成する際コンストラクタに値を渡すことができる
         val factory = HogeViewModel.Factory(
            ...
        )
        ViewModelProvider(this, factory)[HogeViewModel::class.java]
    }

    private lateinit var binding: ActivityHogeBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // データバインド
        binding = DataBindingUtil.setContentView<ActivityHogeBinding>(
            this,
            R.layout.activity_hoge
        ).apply {
            lifecycleOwner = this@BookmarkPostActivity
            vm = viewModel
        }
    }

    ...
}

バインドするデータにLiveDataを使用する場合、作成したbindingに適切なlifecycleOwnerを渡す必要がある。これを行わないとLiveDataの値変更がレイアウトにうまく通知されない。

ViewModelを使うことでsavedInstanceStateはせいぜい初回起動か復元後かを判別する程度の用途しか無くなりましたとさ。良さ。

データの変更をViewに反映する

たとえば次のようなViewModelを用意するとする。
LiveData<T>LiveData<T>.valueが変更された際に生きている監視者に変更を通知するもの。LiveData<T>を継承して好きなように作ることもできるが、ここでは面倒なのでvalueを外から変更できるMutableLiveData<T>を使うことにする。

HogeViewModel.kt

class HogeViewModel(
    private val repository : HogeRepository
) : ViewModel() {
    val text by lazy {
        MutableLiveData<String>("")
    }

    ...

    // NewInstanceFactoryを用意すればViewModelのプライマリコンストラクタでプロパティが初期化できる
    class Factory(
        private val repository : HogeRepository
    ) : ViewModelProvider.NewInstanceFactory() {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>) =
            HogeViewModel(repository) as T
    }
}

ViewModelごとにFactoryを用意しなくてよくするための拡張を書いた▼
コンストラクタ引数有りのViewModelを簡単に作成する - すいはんぶろぐ.io

これを先ほどのレイアウトファイルにTextViewを追加して、データバインディングで表示するようにするには次のようにTextViewを記述する。

<TextView
        android:text="@{vm.text}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

@{...}でViewModel→Viewの単方向データバインディング。

これでviewModel.textが変更されたときにTextViewの表示も変更されるようになる。楽。

Viewの変更をデータに反映する

EditTextの入力テキストを先ほどのHogeViewModel.textに反映する例。

<EditText
        android:text="@={vm.text}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

@={...}でViewModel⇔Viewの双方向データバインディング。

他にもチェックボックスやトグルボタンの状態管理など。

真偽値をバインドしてVisibilityを変更する例

楽そうだと思ったのはこれ。

...
<data>
    <import type="android.view.View"/>
    ...
</data>

...

<TextView
        android:text="@{vm.text}"
        android:visibility="@{vm.display ? View.VISIBLE : View.GONE}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

バインディング式にView.VISIBLEなどを記述する場合、<data>~</data>にてViewをインポートする必要がある。

どうでもいいけど、最近kotlinばかり書いていて三項演算子懐かしいってなった。

レイアウトとバインディング式 | Android Developers

もっと凝ったことをする場合は、バインディングアダプターを用意する方法をとれば良さそう。

バインディングアダプター | Android Developers

双方向バインディングでConverterを介して値をやりとりする

BindingAdapterを使うやり方についてはこちらに書いた。そっちでよさそう。
BindingAdapterに関するいくつかのこと - すいはんぶろぐ.io

たとえば適当な列挙型HogeEnumがあるとして、これをSpinnerに表示して選択させたいときなどに以下のようなシングルトンを用意しておいて、レイアウトから呼ぶことができる。

object HogeEnumConverter {
    @JvmStatic
    @InverseMethod("toHogeEnum")
    fun toInt(item: HogeEnum?) = item?.ordinal ?: 0

    @JvmStatic
    fun toHogeEnum(ordinal: Int) = HogeEnum.values()[ordinal]
}
<layout ...>
    <data>
        <import type="com.suihan74.hoge.HogeEnumConverter"/>
        <variable
                name="vm"
                type="com.suihan74.hoge.HogeViewModel" />
    </data>

    ...

    <Spinner
        android:id="@+id/spinner"
        android:entries="@array/hogeEnums"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:selectedItemPosition="@={HogeEnumConverter.toInt(vm.selectedHogeEnum)}" />

    ...
</layout>

DataBindingのInverseMethodの使い方 - Kenji Abe - Medium

See Also