LoginSignup
58
53

More than 5 years have passed since last update.

Andorid DataBinding with Kotlin

Last updated at Posted at 2017-10-14

実用的なライブラリを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;
    }
}
58
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
53