Google Code Labsの日本語で概要解説の第3弾です。
過去記事はこちら
- Google CodeLabs Android Room with a View - Kotlinを日本語で概要解説
- Google CodeLabs Using Kotlin Coroutines in your Android Appを日本語で概要解説
今回は、Data bindingをやります。ちゃんと勉強したことが無く他人のコードの見よう見まねでしかやったことがなかったので、改めて筋道立てて学んでみます。
元のCodeLabsはこちらの、Android Data Binding codelabです。
しつこいですが、日本語は大いに意訳、要約です(でも面倒になるとGoogleさんに力を借りた直訳風になりますwあと、口語になったり文語になったり)。必ず原文と照らし合わせながら参照してください。
日本語訳以外に、個人的に嵌まったところ(CodeLab内で触れていなくて罠になっているところ)も覚え書きしていきます。
斜体部分が、個人的な感想、メモ、意見、独り言です。(でも、最近気付きましたが、OS、ブラウザによっては斜体って見えないんですね・・・)
対象者
- Java,Kotlinを読める
- Android Architecture Components(以下AAC)のViewModel 、LiveDataについて何となく知っている
- Androidのレイアウトxmlファイルを読める
CodeLab説明
1. Introduction(紹介)
Data Binding Library(Data Bindingライブラリ)
Data Binding Libraryは、レイアウト内のUIコンポーネントとデータ元とを結びつけるのを、プログラム的にではなく宣言的に書けるようにします。このコードラボでは、それを使うためのセットアップ方法と、レイアウトファイルの書き方、obsarvableなオブジェクトと結びつけたり、Binding Adapterをカスタマイズする方法などを学びます。
What you'll build(あなたがビルドするのは)
次のようなアプリをData Bindingを使ったものに変換していきます。
[画像は割愛します]
アプリは1つの画面を持ち、いくつかの固定データ、observableなデータを表示します。つまりデータが変われば、表示も変わらなければなりません。
データはViewModelによって提供されます。Model-View-ViewModel *(※MVVM)*は、Data Bindingととても相性が良いです。ダイアグラムを参照して下さい。
[画像は割愛します]
もし、AACのViewModelクラスにまだ馴染みが無い場合は、(公式ドキュメント)[https://developer.android.com/topic/libraries/architecture/viewmodel]を読んでください。概略を言うと、ViewModelはActivityはFragment
といったViewにUIの状態を提供するクラスです。ViewModelは画面回転でも生き残り、アプリの他のレイヤーに対してあたかもインターフェースかのように動作します。
What you'll need(推奨環境)
Android Studio 3.4以上
下記のプロジェクトをクローンし、実行してください。
$ git clone https://github.com/googlecodelabs/android-databinding
あるいは、サンプルプロジェクトをzipでダウンロドしてください。
- zipを解凍する
- 3.4以上のAndroidStudioでプロジェクトを開く。
[アプリを実行する手段については割愛します]
Likeボタンを押すとカウンターの数字がインクリメント(加算)され、Progress Barも更新されます。SimpleViewModel
を使っています。中身を確認して下さい。
(Windowsの場合)Ctrl+Nでクラスを探すことが出来ます。Ctrl+Shift+Nではファイル名で検索することが出来ます。
Macの場合は、Command+Oでクラス検索、Command+Shift+Oでファイル検索が出来ます。
このショートカット情報はありがたい
SimpleViewModel
は以下のデータを持っています。
- ファーストネームとラストネーム
- イイネの数
- 人気度
また、onLike()
で、イイネの数を加算します。
SimpleViewModel
には興味をそそるような関数はありませんが、それは問題ではありません。一方で、PlainOldActivity
にはたくさんの問題があります。
-
findViewById
をたくさん使っている点。この関数の処理が遅いだけでなく、コンパイル時チェックが無いため安全ではありません。findViewById
に渡したIDが不正な場合、アプリを実行したときにクラッシュします。 -
onCreate
で初期値をセットしいる点。自動的にセットされるデフォルト値がある方が良いです。 -
android:onClick
をレイアウトファイルで指定していますが、これも安全ではない。onLike
メソッドがActivityクラスに無かったり、renameしていたりした場合、アプリ実行時にクラッシュします。 - コードが長い点。ActivityとFragmentはあっという間に肥大化していくので、なるべくたくさんのコードを外に出したいものです。また、AcitivityやFragmentの中のコードは、テストやメンテナンスがしづらいのも難点です。
Data Bindingライブラリを使うと、これらの問題を解決できます。ロジック部分をActivityから分離し、もっとテストがしやすく再利用しやすい場所に出すことが出来るのです。
3. Enable Data Binding and convert the layout(Data Bindingを有効にしてレイアウトを変更する)
このプロジェクトは、既にdata bindingが有効にされています。あなたのモジュールで使うときに最初にすることは、data bindingライブラリを使うように設定することです。
android {
...
dataBinding {
enabled true
}
}
さあ、レイアウトをData Bindingなレイアウトに変えていきましょう。
通常のレイアウトをData Bindingなレイアウトに変えるに必要なステップは、次の通りです。
- レイアウトをタグで囲む
- レイアウト変数を追加する(任意)
- レイアウト式を追加する(任意)
plain_activity.xml
を開いてください。このレイアウトは、ごく標準的な、Constraint Layoutをルートに持つレイアウトです。
Data Binding向けのレイアウトに変えるには、まず、ルート要素を<layout>
タグで囲む必要があります。同時に、namespace定義も(xmlns:
で始まる属性)新しいルートに移動しなければなりません。
お手軽なことに、AndroidStudioで自動的にやってくれる方法があります。
※Const Layoutの開始タグのところで、クリックしてホバリングしていると、黄色い(オレンジかも?)電球アイコンが出るのでそれをクリックするか、Macならalt + Enter
キーでこのメニューが出ます。つーかこれ、知らなかった!
レイアウトはこのようになります。
<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">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
...
<data>
タグを見て下さい。そこにレイアウト変数を入力します。
レイアウト変数は、レイアウト式で使われます。レイアウト式は、要素属性の値として使われ、@{expression}
のフォーマットで指定されます。以下はサンプルです。
// Some examples of complex layout expressions
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
レイアウト式言語はとてもパワフルですが、基本的な記述にだけにすべきです。でないと、レイアウトファイルがとても複雑になり可読性が下がってしまいます。メンテできる人も限られちゃうよ。
// Bind the name property of the viewmodel to the text attribute
android:text="@{viewmodel.name}"
// Bind the nameVisible property of the viewmodel to the visibility attribute
android:visibility="@{viewmodel.nameVisible}"
// Call the onLike() method on the viewmodel when the View is clicked.
android:onClick="@{() -> viewmodel.onLike()}"
完全なドキュメントはこちらを参照して下さい。
いくつかのデータをバインドしていきましょう!
4. Create your first layout expression(最初のレイアウト式を作る)
まず最初は、静的データのバインディングから始めましょう。
-
<data>
タグ内に、2つのレイアウト変数を文字列で定義する
<data>
<variable name="name" type="String"/>
<variable name="lastName" type="String"/>
</data>
-
plain_name
というidのTextViewを見つけ、android:text
属性ををレイアウト式を使って追加する
<TextView
android:id="@+id/plain_name"
android:text="@{name}"
... />
レイアウト式は、{}
内に@
で始めます。
name
は文字列なので、Data BindingはTextViewにどのように値をセットすれば良いかは知っています。他のレイアウト式の種類や属性については、後々学習しましょう。
-
plain_lastName
にも同様にセットする
<TextView
android:id="@+id/plain_lastname"
android:text="@{lastName}"
... />
ここまでの結果は、plain_activity_solution_2.xml.
で見ることが出来ます。
次にData Bindingを正しく反映するよう、Activityを変更します。
5. Change inflation and remove UI calls from activity(レイアウト反映コードを変更し、activityからUI呼び出しを削除する)
レイアウトの準備が出来たましたが、activityのコードも変更する必要があります。PlainOldActivity
を開いて下さい。
Data Binding レイアウトにしたので、レイアウトの作成方法も少し変わります。
onCreate
の、次のコードを、
setContentView(R.layout.plain_activity)
次のように変更します。
val binding : PlainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.plain_activity)
なぜbinding変数を作ったのでしょう? <data>
ブロックで作ったレイアウト変数をセットする手段が必要だからです。それこそが、bindingオブジェクト役目です。Bindingクラスは、ライブラリによって自動的に生成されています。もし興味があるならば、PlainActivitySolutionBinding
クラスを開き、中身を確認して下さい。簡単なコードです。
※PlainActivityBindingがなかなかimport出来ない場合は、Clean & Rebuild, Make Module等して、generatedJavaにBindingクラスが出来ているか確認して下さい。なお、Bindingクラスの命名は、 レイアウトファイル から行われます。
- あとは、値をセットするだけ
binding.name = "Your name"
binding.lastName = "Your last name"
以上です。ライブラリを使って、データをバインドしました。
古いコードを削除出来ます。
-
updateName()
メソッドを削除。Data Bindingは既にレイアウトのIDとセットする値を知っている -
onCreate
でのupdateName()
呼び出しを削除
ここまでの実装結果は、PlainOldActivitySolution2
にあります。
アプリを実行してみましょう。名前が"Ada"から変わったはずです。
※Adaってどこから来たw SampleViewModelでの初期化が、"Grace"ですので、多分そこが途中で変わったのかな(汗) とにかく、変わってます。
6. Dealing with user events(ユーザーイベントを扱う)
さしあたって、データをユーザーに見せる方法は分かりました。しかし、Data Bindingはユーザーイベントをも扱うことができ、レイアウト変数にアクションを発動させることが出来ます。
その前に、少しレイアウトを綺麗にしましょう。
- 最初に、2つのレイアウト変数をViewModelのものに変更する。ほとんどの場合、これにより表示用のコードと状態が1箇所にまとめられる。
<data>
<variable
name="viewmodel"
type="com.example.android.databinding.basicsample.data.SimpleViewModel"/>
</data>
変数に直接アクセスする代わりに、viewmodelのプロパティを使います。
- 両方のTextViewのレイアウト式を変更する
<TextView
android:id="@+id/plain_name"
android:text="@{viewmodel.name}"
... />
<TextView
android:id="@+id/plain_lastname"
android:text="@{viewmodel.lastName}"
... />
"Like"ボタンへの反応もします。
-
like_button
ボタンを探し、次のコードを
android:onClick="onLike"
次のように置き換える。
android:onClick="@{() -> viewmodel.onLike()}"
最初のコードは、ActivityかFragmentに実装されたonLike()
を呼び出すという、安全では無いものでした。(※Fragmentのは自動で呼べなかったような気がするが・・・経験上、Activityにないと必ずクラッシュしていたかと思う・・・) 正しいシグネチャのメソッドがどこにも定義されていないと、アプリはクラッシュしました。
新しい方法はより安全です。コンパイル時に*(関数があるか)*チェックされ、ラムダによってonLike()
が実行時に呼び出されます。
補足内容
AndroidStudioで"Make Project"をすればData Bindingのエラーも見られるよ。
ここまでの実装結果は、plain_activity_solution_3.xml
で見ることが出来ます。
activityのコードから、もはや不要となったコードを削除しましょう。
-
下記のコードを、
PlainOldActivity.ktbinding.name = "Your name" binding.lastName = "Your last name"
次のように変更する。
PlainOldActivity.ktbinding.viewmodel = viewModel
-
onLike
メソッドを削除する。処理のバイパスは既になされている
ここまでの実装結果は、PlainOldActivitySolution3.kt
に見ることが出来ます。
アプリを実行すると、ボタンをタップしても何も起こらないでしょう。updateLikes()
が呼ばれなくなったからです。その実装をしていきましょう。
※ViewModelは画面回転で生き残ります。Activityは再生成されます。ということは、onCreateが呼ばれます。onCreate内では、updateLikes()
を呼んでいるので、そこでLikesカウントやPopularity画像が変わっているのは確認できます。
7. Observing data(データを監視する)
前回、静的なデータのバインドを行いました。view modelクラスを見れば、name
とlastName
はただの文字列型だと分かるでしょう。それは変わることはないので、それで良いのです。一方、likes
はユーザー操作により変更されます。
var likes = 0
この値が変更されたときに、UIを明示的に更新するのではなく、observable(観察可能)にします。
Data Bindingでは、observableなデータが変更されると、UIも自動的に更新されます。
データをobservableにするには、いくつかの方法があります。observableクラスや、observable fieldsもありますが、 ここではよりLiveData
の方が好ましいでしょう。詳細はこちらを見て下さい。
ObservableFields
がより単純なので使用します。
※え!? どう見ても、下でLiveData
使ってるけど!??! しかもその前にLiveData
が好ましい言うてるやん!? ということで、これはLiveData
の間違いかと思います。LiveData
が出てくる前の記述の修正漏れ?
次のコードを置き換えます。
val name = "Grace"
val lastName = "Hopper"
var likes = 0
private set // This is to prevent external modification of the variable.
このようにします。
private val _name = MutableLiveData("Ada")
private val _lastName = MutableLiveData("Lovelace")
private val _likes = MutableLiveData(0)
val name: LiveData<String> = _name
val lastName: LiveData<String> = _lastName
val likes: LiveData<Int> = _likes
また、次の箇所も直します。
fun onLike() {
likes++
}
/**
* Returns popularity in buckets: [Popularity.NORMAL],
* [Popularity.POPULAR] or [Popularity.STAR]
*/
val popularity: Popularity
get() {
return when {
likes > 9 -> Popularity.STAR
likes > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
以下のようにします。
// popularity is exposed as LiveData using a Transformation instead of a @Bindable property.
val popularity: LiveData<Popularity> = Transformations.map(_likes) {
when {
it > 9 -> Popularity.STAR
it > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
}
ご覧の通り、LivDataはsetValue()
で値がセットされる必要があります*(※Kotlinではsetterはプロパティアクセスに書き換えられるので、_likes.value = XXX
は_likes.setValue(XXX)
と同義)*。そして他のLiveDataに依存したLiveDataをTransformationsを使って定義することが出ます。この仕組みが、ライブラリが値が変わったときにUIを更新出来るようにしています。
LiveDataはライフサイクルを意識したobservableなオブジェクトなので、ライフサイクルオーナーを指定しなければなりません。これはbinding`オブジェクトに対して行えます。
PlainOldActivity
クラスを開き(現時点で、このクラスはPlainOldActivitySolution3
と同等になっているべきです)、ライフサイクルオーナーをbinding
に設定します。
binding.lifecycleOwner = this
プロジェクトをリビルドすると、activityがコンパイルエラーになるでしょう。activityからlikes
に直接アクセスしてしまっていますが、これはもう不要です。
private fun updateLikes() {
findViewById<TextView>(R.id.likes).text = viewModel.likes.toString()
findViewById<ProgressBar>(R.id.progressBar).progress =
(viewModel.likes * 100 / 5).coerceAtMost(100)
...
PlainOldActivity
のprivate関数はすべて不要になったので、宣言と呼び出し箇所を削除します。activityクラスのコードが可能な限りシンプルになりました。
class PlainOldActivity : AppCompatActivity() {
// Obtain ViewModel from ViewModelProviders
private val viewModel by lazy { ViewModelProviders.of(this).get(SimpleViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding : PlainActivityBinding =
DataBindingUtil.setContentView(this, R.layout.plain_activity)
binding.lifecycleOwner = this // CodeLabsのコードはここが抜けているので必要です
binding.viewmodel = viewModel
}
}
※えーと、binding.lifecycleOwner = this
が抜けてますよね。必要です。
ここまでの実装を終えると、SolutionActivity
と同じになっているはずです。
activityクラスからコードを外部へ移動していくことは、通常、メンテナンス性と可読性という意味で非常に効果的です。
TextView
をバインドして、イイネの数を、監視している数値を表示しましょう。plain_activity.xmlを次のようにしましょう。
<TextView
android:id="@+id/likes"
android:text="@{Integer.toString(viewmodel.likes)}"
...
アプリを実行して、ボタンをタップすると、期待通りに、カウントが増えていきます。
========================================================
重要補足コメント
そのままこの工程をやってくると、ここでビルドエラーになります。
SimpleViewModel
の各変数をLiveDataにしたので、SimpleViewModel
を参照していた、以下のクラスでコンパイルエラーになるのです。
- PlainOldActivitySolution2
- PlainOldActivitySolution3
上記のクラスからもすべてのprivate関数と呼び出し箇所を削除するか、SimpleViewModel
をコピーしてSimpleNewViewModel
とかにして、PlainOldActivity
はそれを使うようにするとかすれば通るようになります。
ビルドしてないやろ、このプロジェクト作成者(汗)
========================================================
ここまでやってきたことを総括します。
- Nameとlast nameをただの文字列としてView Modelから表示
- ボタンの
onClick
属性を、ラムダ表記でView Modelとバウンド - イイネの数をview modelのInt型のデータを監視し、TextViewと結びつけることで、変更があると自動的に表示が更新されるようにした
android:onClick
やandroid:text
といった属性を使ってきました。それ以外の属性や、自作することにについて見ていきましょう。
8. Using Binding Adapters to create custom attributes(BindingAdapterを使ってカスタム属性を作る)
文字列データ(あるいはそのLiveData)をandroid:text
属性ににバインドすると何が起こるかは明白ですが、では、それはどうやって行われているのでしょう?
Data Bindingライブラリでは、ほとんどのUI呼び出しは、Binding Adaptersと呼ばれるstaticメソッドによって行われます。
ライブラリは膨大な数のBinding Adaptersを提供しています。ここでチェックできます。android:text
属性への例はこうなっています。
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
// Some checks removed for clarity
view.setText(text);
}
また、android:background
であればこうなっています。
@BindingAdapter("android:background")
public static void setBackground(View view, Drawable drawable) {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
view.setBackground(drawable);
} else {
view.setBackgroundDrawable(drawable);
}
}
Data Bindingにマジックはありません。すべてはコンパイル時に解決され、ジェネレーとされたコードで読むことが出来ます。
progress barについて実装していきましょう。以下の動作にしたいものです。
- イイネが無ければ非表示にする
- 5イイネで満タンにする
- 満タンで色を変える
これのためにカスタムBinding Adapterを作成していきます。
utilパッケージのBindingAdapters.kt
を開いて下さい。ファイルはパッケージのどこにあっても問題ありません。ライブラリが見つけてくれます。Kotlinにおいては、staticメソッドはファイルのトップレベルに定義するか、クラスの拡張関数として定義することが出ます。
最初の問題を解決するための、Binding Adapterを見て下さい。それはhideIfZero
です。
@BindingAdapter("app:hideIfZero")
fun hideIfZero(view: View, number: Int) {
view.visibility = if (number == 0) View.GONE else View.VISIBLE
}
このアダプターは、以下のことをします。
-
app:hideIfZero
属性に当てはめる - すべてのViewに適用できる(最初の引数が
View
クラスだから。この型を変えることで限定することも可能) - レイアウト式が返すべき値として、整数を取る
- 値がゼロならViewのvisibilityをGONEに設定する。そうで無ければVISIBLEを設定する
plain_activity
レイアウトで、progress barを探してhideIfZero
属性を追加しましょう。
<ProgressBar
android:id="@+id/progressBar"
app:hideIfZero="@{viewmodel.likes}"
...
アプリを実行すると、Likeボタンを初めて押したときにprogress barが現れるのを見て取ることが出来ます。でも、まだ値や色を変える必要があります。
ここまでの実装の完成形は、plain_activity_solution_4.xml
にあります。
9. Create a Binding Adapter with multiple parameters(複数の引数を取るBinding Adapterを作る)
(Progress Barへの)進捗値のために、最大値とイイネの数を取るBinding Adapterを使いましょう。BindingAdapters
を開き下記のコードを探して下さい。
/**
* Sets the value of the progress bar so that 5 likes will fill it up.
*
* Showcases Binding Adapters with multiple attributes. Note that this adapter is called
* whenever any of the attribute changes.
*/
@BindingAdapter(value = ["app:progressScaled", "android:max"], requireAll = true)
fun setProgress(progressBar: ProgressBar, likes: Int, max: Int) {
progressBar.progress = (likes * max / 5).coerceAtMost(max)
}
このBinding Adaptersは、すべての属性が揃っていないと、使われません。これはコンパイル時に起こります。メソッドは3つの引数を取っています。アノテーションで定義された属性の数がViewに適用されます*(※ここの英文意味不明なんですが・・・)*
requireAll
はbinding adapterが使われたときの動作を決定します。
-
true
にすると、すべての属性がXMLで定義されている必要がある -
false
にすると、未定義の属性はnullか、booleanの場合はfalse, プリミティブ型の場合は0として扱われる
XMLに次の属性を追加しましょう。
<ProgressBar
android:id="@+id/progressBar"
app:hideIfZero="@{viewmodel.likes}"
app:progressScaled="@{viewmodel.likes}"
android:max="@{100}"
...
progressScaled
属性を、イイネの数ででバインドしました。max
にはただの数値を渡しています。@{}
フォーマットを使わないと、Data BindingはどのBinding Adapterを使えばいいか分からなくなります。
ここまでの実装結果は、plain_activity_solution_5.xml
にあります。
アプリを実行すると、progress barが期待通りに動作しているのを見られます。
10. Practice creating Binding Adapters(Binding Adapters作成の練習)
練習は大事だよ。次の物を作ろう。
- イイネの数でprogress barの色を変え、対応する属性をバインドするBinding Adapter
- popularityに応じて異なるアイコンを表示するBinding Adapter
-
ic_person_black_96dp
を黒で -
ic_whatshot_black_96dp
をライトピンクで -
ic_whatshot_black_96dp
をゴールドピンクで
答えは、BindingAdapters.kt
ファイル、SolutionActivity
、そしてsolution.xml
レイアウトにあります。
※BindingAdapters.ktに、必要なメソッドは用意されているので、実際はBindAdapterを作る必要がありません。また、この段階でActivityには追加/変更するコードはありません。まあつまり編集する必要があるのはxmlだけです。
11. You're bound to succeed!(あなたの成功は約束された!)
おめでとうございます! codelabを完了したあなたは、Data Binding レイアウト、変数、そして式を使う方法を知ったはずです。observable dataを使い、カスタム属性とBinding AdapterとでXMLレイアウトをもっと重要なものとすることも。
最後に
感想
やってみると意外に簡単ですね。食わず嫌いはやっぱりダメですね^^;
特にViewModelのLiveDataでの使い方は、今後それらが主流になることを考えると、おおいに参考になりそうです。
あまり複雑な式は書かない方が良さそうですが、積極的に使っていこうと思います。