Edited at

Andorid DataBinding with Kotlin

More than 1 year has passed since last update.

実用的なライブラリをKotlin交えて紹介しようプロジェクト1

昨今のAndroidプロジェクトでは必須でもある

DataBindingについて、使い方をあらっていこうと思います。


What is Data Binding?

Googleが提供している公式ライブラリ

レイアウトにデータを流すことができる便利なライブラリです

Android標準の方法ですと

ソース上でViewを取得し、それに対してデータを設定する必要がありますが

DataBindingを使うことでもっと簡単にレイアウト上のデータを反映することができます。


Example Code

たとえば、Viewに対しての操作をActivity上でこう書いていたとします。

val titleView = findViewById(R.id.title) as TextView

titleView.setText(data.title)
titleView.setTextColor(data.color)
val subTitleView = findViewById(R.id.subTitle) as TextView
subTitleView.setText(data.subTitle)
subTitleView.setTextColor(data.color)

: (省略)

val progressBar = findViewById(R.id.progress) as ProgressBar
progressBar.setVisibility(if (data.loading) View.VISIBLE else View.INVISIBLE)
val titleTextView = findViewById(R.id.title) as TextView
titleTextView.setVisibility(if (data.loading) View.INVISIBLE else View.VISIBLE)
val subTitleView = findViewById(R.id.subTitle) as TextView
subTitleView.setVisibility(if (data.loading) View.INVISIBLE else View.VISIBLE)

うっとおしいですよねこれ。

ぼくも大嫌いでした。

しかしこれが、ソース上、こんなにスッキリ書けます。

val binding = DataBindingUtil.setContentView(R.layout.activity_main)

binding.rowData = data

そして代わりに、どんなときにどんな値をもつかを

レイアウト上に記述することが可能になります。

<!-- For title -->

<TextView
...
android:textColor="@{data.color}"
android:text="@{data.title}" />

<!-- For subtitle -->
<TextView
...
android:textColor="@{data.color}"
android:text="@{data.subtitle}" />

<!-- For loading progress -->
<ProgressBar
...
android:visibility="@{data.loading ? View.VISIBLE : View.INVISIBLE}" />


メリット

あくまで個人の意見です


ViewとデータModelを簡単に分けられる

レイアウトファイルに何のデータを表示するかを記載し、

ソースにはレイアウトにデータを流すコードだけを記載するのが基本です。

そのため、実装をしていると自然とViewとデータModelクラスが分離していきます。

こうするとそれぞれがシンプルな形となり、

テストからメンテナンスまでしやすくなります。

そう考えると、非常に導入するメリットが大きいかと思います。

昨今よく聞くMVVMとの融和性が非常に高いです。

Kotlinを使うと、各レイヤー間のやりとりも高階関数で補えるのがまたGoodです。

1-VLhXURHL9rGlxNYe9ydqVg 2.png

参照サイト


View取得が楽

DataBindingを使うと、Viewのキャストミスもなくなりますし、

コードも見やすくなります。

Android標準では、findViewById を使って取得できる

Viewインスタンスをキャストする必要があります。

val titleTextView = findViewById(R.id.title) as TextView

titleTextView.setText(data.title)

しかし、DataBindingだと簡単にキャストされた形でViewを取得できます

// binding.title が、id=titleのTextViewを指す

val binding = DataBindingUtil.setContentView(R.layout.activity_main)
binding.title.setText(data.title)


なんたって公式

昔はButterKnifeというライブラリが

DataBindingのような機能を提供していました。

広く使われていたとも聞きます。

しかし2年ほど前からGoogleが公式サポートしましたので、

どんどんこちらへ移行しているのが実情のようです。

やっぱり公式だと安心ですしね。


デメリット

これといって思いつかなかったんですが、

強いて言うなら...


エラーが見づらくなる

DataBindingのライブラリは、自動生成されるクラスを介して利用します。

しかし、コンパイルエラーが発生すると、これらのUtilクラスがなくなってしまうため、

見たいエラーがDataBinding用のクラス参照エラーで埋もれてしまいます。

だから注意してコーディングしなはれや!それだけ。


慣れるまで見づらい

処理をレイアウトとコードどちらにも書くため、

見るソースが単純に増えます。

そのため、とくに初心者の方からすると、

最初は見づらく感じるかもしれません。

しかし、アプリ(プロジェクト)が大きくなるにつれ

そのありがたみもわかりますし、そのうち解消されるでしょう。


基本

ではいよいよ使ってみましょう。

いくつか手順があります。


  1. gradleファイルに宣言

  2. レイアウトに <layout> タグをつける

  3. コード上でBindingクラスを取得

  4. Bindingインスタンスを使って操作


gradleファイルに宣言

アプリのGradleファイルに、以下を加えてSyncしましょう

apply plugin: 'kotlin-kapt'

android {
...(省略)...
dataBinding {
enabled = true
}
}

dependencies {
kapt "com.android.databinding:compiler:2.3.3" //数値はGradleBuildバージョン
}

これで準備OKです


レイアウト宣言

レイアウトファイルの中を<layout>タグでくくることで

DataBindingを利用する対象のレイアウトという宣言ができます。


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>

こんな感じ


Bindingインスタンス取得

では、実際にこのレイアウトにアクセスするための

Bindingインスタンスを取得します

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

// with setContentView
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
}

DataBindingUtil ... DataBindingのライブラリがもともともつUtilityクラス

ActivityMainBindng ... レイアウトファイルを<layout>タグで囲むことで自動生成されるクラス。レイアウトファイル名+Bindingになる。

Bindingインスタンスを取得する方法はいくつかあるんですが、

inflateするだけなどの場合はこう書きます

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {

val binding = convertView
?.let { DataBindingUtil.findBinding(convertView) } // ViewからBindingインスタンス取得
?: RowListBinding.inflate(mLayoutInflater) // inflateするだけ
return binding.root
}


id参照

例えばこのようにレイアウトファイルを生成し、

Buttonにidを設けたとします

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>

するとBindingインスタンスを介してこのようにアクセスできます

val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

binding.button.text = "Button Text"

このときのbinding.buttonは参照時点ですでにButtonクラスにキャストされています。

ちなみに、本当にソース責務を分離するのであれば、

レイアウトにidを入れることがなくなるそうです。

(id入れるってことは、別のレイヤーソースで使うってことでしょ的な考え)


データ挿入

では実際にレイアウトにデータを流す方法ですが、

方法がいくつかあるので、用途によって使い分けましょう


Basic


レイアウト:データ宣言

レイアウトにdataを定義する必要があります

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="data" type="com.android.example.Data" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.title}" />
</LinearLayout>
</layout>

<data> がいわゆるデータを宣言するエリア

<variable> がデータの宣言、

その中でもnameはレイアウトファイル内での変数名のようなもの、

typeはクラスの宣言になります。(もちろんインターフェースでも問題ありません)


レイアウト:データ反映宣言

ではどこに何を反映させるか、という書き方ですが

レイアウトファイルの @{} で囲まれた箇所がそれにあたります。

この場合、ButtonのテキストにDataクラスのもつ title という要素を入れる、という表現になります

もっと言えば、ButtonのSetterメソッドに渡す値を @{} で指定します


データ挿入

対応するレイアウトBindingクラスに set+VariableのName メソッドができるので

それにデータを指定することでデータをレイアウトに流すことができます

val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

val data = Data()
binding.data = data // Javaだとbinding.setData(data);

すると、このタイミングでレイアウト上にtitle がButtonに反映されます


データ監視

挿入したデータを、動的に変化させたい!

というときに利用します。


Observable(監視)

双方向のデータ通信方法その1

たとえばEditTextの入力内容を監視するときに使います。


レイアウト

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="data" type="com.android.example.Data" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.title}" />
</LinearLayout>
</layout>


コード

データModelクラスはこのように書きます

data class Data(val title: ObservableField<String>) {

init {
title.set("")
}
}

ObservableField は型が指定できますが、少し重いらしいです。

ObservableBoolean、

ObservableByte、

ObservableChar、

ObservableShort、

ObservableInt、

ObservableLong、

ObservableFloat、

ObservableDouble、

ObservableParcelable

といった各種変数のObservableクラスが用意されているので、

可能な限り、そちらを利用しましょう。


Bindable(監視)

双方向のデータ通信方法その2

その1で実装できないパターンはこちらを使います。

たとえばEditTextの入力内容が変わったときに何かしたい!というときに使えます。

なぜか動く時と動かないときがあります。。調査中


レイアウト

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="data" type="com.android.example.Data" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={data.title}" />
</LinearLayout>
</layout>


コード


  • BaseObservableを継承する

  • 監視したいGetter/Setterに@Bindableをつける

class Data: BaseObservable() {

var title: String = ""
@Bindable get() = field
@Bindable set(value) {
field = value
notifyPropertyChanged(BR.title)

// +アルファ
}
}


応用(+実例)


イベント処理


リスナーBinding

onClickなどのリスナーで、ラムダ式のように記述をして

直接メソッドコールさせることが可能です。

レイアウトにリスナーを書くようなイメージでしょうか。

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>


メソッド参照

ClassName::MethodName の形で、

メソッドの参照をさせることが可能です。

class MyHandlers {

fun onClickFriend(view: View) { ... }
}

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.Handlers"/>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
android:onClick="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>


注意

書けるのはいいんですが、

複雑なものをこうして書くと返って読みづらくなります。

ほどほどにしておきましょう!


レイアウト詳細


import

フレームワークにある定数などを利用する場合、

レイアウトで利用するクラスとしてimportすることができます

<?xml version="1.0" encoding="utf-8"?>

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

<data>
<import type="android.view.View" />
<variable
name="task"
type="com.example.Task" />
</data>
...
<ProgressBar
...
android:visibility="@{task.loading ? View.VISIBLE : View.INVISIBLE}" />

例えばListのimportなどもこのように行います

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

xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.example.model.Person" />
<import type="java.util.List"/>
<variable name="person" type="Person"/>
<variable name="persons" type="List&lt;Person&gt;"/>
</data>

ただ注意点として、現在のDataBindingライブラリだと

ビルドは通りますが、IDEのエディタでの表示上、エラーがでてしまいます。


Bindingクラス名の変更

あんまり使わないですが、自分で決めた名前でBindingクラスを作ることが可能です。

<data class="ContactItem">

...
</data>


include

レイアウトファイルの中で、他レイアウトを<include>してる方もいると思います。

その場合でも心配無用、きちんとデータを渡す方法があります。

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>

bind:xxxx としたxxxx部分が、

include先のdataのname定義になります。

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<merge>
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>


式言語

以下は普通に@{}の中で利用することが可能です

簡単な条件分岐や計算であれば、書いた方がいいでしょう


  • 算術演算子 + - / * %

  • 文字列連結演算子 +

  • 論理演算子 && ||

  • バイナリビット演算子 & | ^

  • 単項演算子 + - ! ~

  • シフト演算子 >> >>> <<

  • 比較演算子 == > < >= <=

  • instanceof

  • グルーピング ()

  • リテラル: 文字、文字列、数字、null

  • キャスト

  • メソッド呼び出し

  • フィールド アクセス

  • 配列アクセス []

  • 三項演算子 ?:


null合体演算子

NULLケースを踏まえたケースを??を使うことで表現できます

<TextView

android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{data.name ?? @string/no_name}"
tools:text="Title"/>


リソース参照

先出ししてしまいましたが、@stringなどのリソース参照も普通にできます。

引数を利用する場合においても活用可能です。

<TextView

android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{data.valid ? @string/name_with_tag(data.name) : @string/no_name }"
tools:text="Title"/>

<string name="no_name">NO NAME</string>

<string name="name_with_tag">NAME: %s</string> <!-- 引数がひとつのケース -->
<string name="name_with_tag2">NAME: %1s, %2s</string> <!-- 引数がひとつのケース -->

もちろん、@color@drawableなども可能です。

場合によって色分けやアイコンを変える時などは便利ですね。


Bindingの同期

DataBindingは基本的にバックグラウンドで

適当なタイミングで実行されます。

executePendingBindings()を利用すると、

その場でbindingの同期をとってくれます


BindingAdapter

BindingAdapterというアノテーションが用意されており、

これを利用することでレイアウトに対するSetterを宣言することができます。

たとえばAndroidの android:paddingLeft は以下のように定義されています

@BindingAdapter("android:paddingLeft")

public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}

こいつらは以下を守っていれば、どこに定義してあげてもいいです。

(Utilクラスを作って定義することが一般的かとは思いますが。。)

- BindingAdapterアノテーションを定義している

- public static なメソッドである

- 第一引数は対象のViewクラス、第二引数以降はBindingAdapterの定義順

たとえば複数の引数をとりたい場合はこのように書きます

@BindingAdapter({"listData", "onItemClick"})

public static void setCustomAdapter(ListView listView, List<String> listData, OnItemClickListener listener) {
listView.setAdapter(new CustomAdapter(listData, listener));
}

<ListView

android:width="match_parent"
android:height="match_parent"
app:listData="@{data.listData}"
app:onItemClick="@{activity}"

Kotlinの場合はExtensionで定義させることも可能です。

@BindingAdapter("data")

fun ListView.loadAdapter(data: List<Person>) {
BMIListAdapter(this.context, data).also {
this.adapter = it
this.onItemClickListener = it
}
}

余談ですが、私の担当しているプロジェクトではKotlinで書けません。

導入しているライブラリが影響しているのか、Javaと混ぜて書いてるかはわかりませんが。。

Kotlinだけで書いてるプロジェクトの方は問題なく使えるはずです!


DataBindingのsafeUnbox()の使い所

app:answer="@{visitReport.answer != null ? visitReport.answer : 0}"

こういう定義をしていて、answerがIntegerの場合、

JavaコードだとこれでOKだけど、DataBinding使ってレイアウト上でこれ記述すると、

たぶんコンパイル方法の関係で ? visitReport.answer の箇所で

「NPE発生の可能性あり」と警告がでてしまう。

そんなときのために DataBindingではUtilityクラスが用意されている模様。

なのでデフォルトが0のときなどはこれ使えばOKみたい。

package android.databinding;

public class DynamicUtil {
public static int safeUnbox(java.lang.Integer boxed) {
return boxed == null ? 0 : (int)boxed;
}
public static long safeUnbox(java.lang.Long boxed) {
return boxed == null ? 0L : (long)boxed;
}
public static short safeUnbox(java.lang.Short boxed) {
return boxed == null ? 0 : (short)boxed;
}
public static byte safeUnbox(java.lang.Byte boxed) {
return boxed == null ? 0 : (byte)boxed;
}
public static char safeUnbox(java.lang.Character boxed) {
return boxed == null ? '\u0000' : (char)boxed;
}
public static double safeUnbox(java.lang.Double boxed) {
return boxed == null ? 0.0 : (double)boxed;
}
public static float safeUnbox(java.lang.Float boxed) {
return boxed == null ? 0f : (float)boxed;
}
public static boolean safeUnbox(java.lang.Boolean boxed) {
return boxed == null ? false : (boolean)boxed;
}
}