実用的なライブラリを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です。
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用のクラス参照エラーで埋もれてしまいます。
だから注意してコーディングしなはれや!それだけ。
慣れるまで見づらい
処理をレイアウトとコードどちらにも書くため、
見るソースが単純に増えます。
そのため、とくに初心者の方からすると、
最初は見づらく感じるかもしれません。
しかし、アプリ(プロジェクト)が大きくなるにつれ
そのありがたみもわかりますし、そのうち解消されるでしょう。
基本
ではいよいよ使ってみましょう。
いくつか手順があります。
- gradleファイルに宣言
- レイアウトに
<layout>
タグをつける - コード上でBindingクラスを取得
- 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を利用する対象のレイアウトという宣言ができます。
<?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<Person>"/>
</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;
}
}