Android DataBinding の使い方

codelab android databinding

Android DevSummit 2019 の codelabs が google codelabs に追加されていたので、 DataBinding を改めて勉強しました。

codelabs はこちら

そもそも DataBinding を使う理由としては、 codelabs には以下のように書かれています。

  • findViewById() を使うのはよくない
    • 処理速度が遅い
    • コンパイル時に例外検査がないので安全ではない
      • layout xml に存在しない id を渡してしまうと実行時にクラッシュする
    • findViewById() だと onCreate 時に初期値が決まるが、これより DataBinding でデフォルト値を設定して自動的に反映させるほうが良い
  • Button の android:onClick attribute に直接 onClick 時に実行する method 名を書くのは良くない
    • コンパイル時に例外検査がないので実行時にクラッシュしてしまう
    • また、 rename 時に見逃しやすい

そのため、 DataBinding を使ってない場合は、現在動いているコードは積極的に書き換えなくても良い気がしますが、新たに追加する画面などでは DataBinding を使っていく感じにしたほうが良さそうです。

DataBinding 使用方法

build.gradle

build.gradle に以下のコードを追加して dDataBinding を有効化します

android {
...
    dataBinding {
       enabled true
    }
}

layout xml

layout xml はルートタグが <layout> タグであれば DataBinding が使用できます。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

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

AndroidStudio では、一番上の親要素にカーソルを合わせると以下の画像のようにダイアログが出てくるので Convert to data binding layout を選択すると自動的に DataBinding が使用できる状態に変換してくれます。

f:id:Nshiba:20191117143420p:plain
Convert to data binding layout

Activity で使うには onCreate 内の setContentView() を以下のように置き換えます

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

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

これで DataBinding を使用する準備はできました。

レイアウト変数/レイアウト式

DataBinding を使用すると xml 上で変数と式を使用することができるようになります。

  • 変数は data タグ内で宣言します
  • data タグ内では import 文を書くことが出来て、 import するとレイアウト式で import したクラスのメソッドが使用することができます。
  • レイアウト式を使うには android:text=@{text} という形で書きます。
  • レイアウト式ではラムダ式も使えるため、 android:onClick="@{() -> viewModel.onClick()}" という形で書くことが出来ます。
    • このようにラムダ式で書くとコンパイル時の例外検査が走るので、実行前にメソッドがあるかどうかのチェックができるようになります
<layout>

    <data>

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

        <variable
            name="viewModel"
            type="net.nshiba.databindingsample.MainViewModel" />
    </data>

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

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.name + String.valueOf(viewModel.age)}"
            android:visibility="@{viewModel.age &lt; 13 ? View.GONE : View.VISIBLE}"
            android:onClick="@{() -> viewModel.onClick()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

あとは Activity 側でデータを設定してあげるだけです。

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private val viewModel: MainViewModel by viewModels()

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

        ここで viewModel を設定
        binding.viewModel = viewModel
    }
}

またここでは、 ViewModel を使用しています。
ViewModel を使用するとデータを View に反映する前段階の処理と状態の保持を一箇所にまとめることができます。
ちなみにここで使っている MainViewModel の中身は以下の感じです。

class MainViewModel : ViewModel() {

    val name: String = "nshiba"

    val age: Int = 15

    fun onClick() {
        // do nothing
    }
}

注意点

DataBinding はとても便利です。 しかし、 layout xml に複雑なロジックを書いてしまうと、可読性とメンテナンス性が下がってしまうためバランスをとって書きましょう

LiveData

LiveData を使うとデータの変更を監視して、変更があったら自動的にUIに反映させることが可能です。

使い方は簡単で、以下のように MutableLiveData を使って値を宣言し layout に設定するだけです。

class MainViewModel(): ViewModel() {

    ...

    private val likes = MutableLiveData(0)
    
    fun onClick() {
        likes.value = (likes.value ?: 0) + 1
    }
}
        ...
    
        <TextView
            android:id="@+id/likes"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@{String.valueOf(viewModel.likes)}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/button" />
        
        ...

また LiveData は LifeCycle を知っている必要があるので Activity/Fragment で DataBinding に対して LifeCycleOwner を設定してあげる必要があります

   override fun onCreate(savedInstanceState: Bundle?) {
        ...
        
        binding.lifecycleOwner = this
    }

こうすると、最初に作った Button のクリックリスナーに viewMode.onClick() が設定されているため、ボタンを押すと like が増えていくようになりました。

他にも LiveData には Transformations.map() というメソッドを使うと、データに変更があったらそのデータを加工して違うデータにすることが可能です。

class MainViewModel(): ViewModel() {
    ...
       
    val popularity = Transformations.map(likes) {
        when {
            it > 9 -> "START"
            it > 4 -> "POPULAR"
            else -> "NORMAL"
        }
    }
    
    ...
}

BindingAdapter

BindingAdapter を使うと簡単にカスタム attributes を作ることができます。
まず、 BindingAdapter を使うには build.gradle に apply plugin: 'kotlin-kapt' を追記してください。

次に BindingExt.kt ファイルを作り @BindingAdapter annotation がついたメソッドを作ります。

@BindingAdapter("app:visibleFromLike")
fun View.setVisibleFromLike(likes: Int) {
    visibility = if (likes < 4) {
        View.GONE
    } else {
        View.VISIBLE
    }
}

@BindingAdapter に指定した引数を layout xml で使用することができます

<TextView
    android:id="@+id/popularity"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:text="@{viewModel.popularity}"
    app:visibleFromLike="@{viewModel.likes}"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/likes" />

これで lives < 4 のときに現れる View を作ることができました。

また、 BindingAdapter は複数の attributes を設定することができます

@BindingAdapter(value = ["app:visibleFromLike", "app:visibleFromLikeMax"], requireAll = false)
fun View.setVisibleFromLike(likes: Int?, max: Int?) {
    ...
}

value に設定する項目を配列にして、メソッドの引数を配列のサイズと同じにします。
ここで requireAll = true を指定すると、すべての attributes が View に設定されていなければコンパイルエラーが発生するようになります。

まとめ

DataBinding を使うと Activity/Fragment 側で UI 変更のコードがなくなりすっきりしていいですね。
Android アプリ開発において DataBinding を使うかどうかは結構好みが分かれると思いますが、がっつり使うとかなり便利になるので個人的にはおすすめです。

ただ、 findViewById() の置き換えとして使うだけだとビルド時間も伸びるので、その場合は ViewBinding を使うほうが良いと思います。

DataBinding についてもっと詳しくやりたい方は、とりあえず実際に codelabs をやってみると良いと思います。
https://codelabs.developers.google.com/codelabs/android-databinding/#0

今回作成したサンプルプロジェクトはこちらです。
https://github.com/nshiba/DataBindingSample