##はじめに
データバインディングの基本的な部分について、公式ドキュメントを読みつつコードを書いてまとめてみました。
公式ドキュメントが分かりやすくて素晴らしい
##この記事で書くこと
- データバインディングの導入
- データバインディング可能なレイアウトを作る
- データを埋め込む
- バインディングクラスの紐づけ
- ViewModelでデータを管理する
- 監視可能なフィールド(Observable)を使用して、データが変わったときにUIに自動で反映する
##そもそもデータバインディングライブラリとは何か
AndroidJetpack(Androidアプリを簡単に作成するための一連のライブラリ、ツール、およびガイダンス)の一部。プログラムではなく宣言形式を使用して、レイアウト内のUIコンポーネントをアプリのデータソースにバインドできるサポートライブラリ。
https://developer.android.com/jetpack?hl=ja
https://developer.android.com/topic/libraries/data-binding?hl=ja
ざっくり補足と覚え書き
※AndroidJetpackは、androidx.*パッケージライブラリで構成されている。
※"プログラムではなく宣言形式を使用して"というのは、
val text1 = findViewById<TextView>(R.id.text1)
というように一つずつUIにバインドするのではなく、
下記のようにレイアウト内で宣言する形でバインドできるということ。
android:text="@{horoscope.sign}"
##導入するには
Android SDK Manager の Support Repository からデータバインディングライブラリをダウンロードしておく。
アプリで使用するには、build.gradleにデータバインディングを使用する定義を追加すればよい。
android {
...
dataBinding {
enabled = true
}
}
##データ バインディングのレイアウトをつくる
データバインディング可能なレイアウトを作るには、以下のような階層でタグを配置する。
dataタグでは、このレイアウト内で使用したいプロパティを定義する。
- layoutタグ
- dataタグ
- Viewのrootタグ
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="horoscope"
type="com.yayame.databindingapp.model.Horoscope" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
レイアウトを作った後ビルドすると、データバインディングライブラリがバインディングクラスを自動生成してくれる。
生成されたクラスの名前はパスカルケース+Bindingとなる。
例)activity_main.xml → ActivityMainBinding
##データを埋め込むには
@{}の構文を利用して、データを埋め込む。
ここでは星占い(Horoscope)クラスの星座(sign)を取得してtextに埋め込んでいる。
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{horoscope.sign}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
data class Horoscope(val sign: String, val health: String, val money: String, val love: String)
##バインディングクラスの紐づけ
最後に、バインディングクラスにデータクラスを紐づける。
上記で作成したバインディングクラスのinflate()を使うことで、ビュー階層ごとオブジェクトをバインドできる。
※ただし、Fragment、ListView、RecyclerViewのアダプター内でバインディングしたものを使用している場合は、DataBindingUtilクラスのinflate()メソッドを使うのが良いらしい。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val horoscope = Horoscope("Aries", "good", "bad", "very good")
// test2
val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.horoscope = horoscope
}
##レイアウト内で式言語も使える
レイアウトの@{}の中で、式言語を使うことも可能。
ここでは、dataタグ内でViewをインポートし、horoscope.money
が特定の値だったらvisibilityにView.GONE
を設定して表示させないようにしている。
<?xml version="1.0" encoding="utf-8"?>
<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>
<import type="android.view.View"/>
<variable
name="horoscope"
type="com.yayame.databindingapp.model.Horoscope" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<TextView
android:id="@+id/text3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{horoscope.money}"
android:visibility='@{horoscope.money.equals("bad") ? View.GONE : View.VISIBLE}'
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ViewModelでデータを管理する
レイアウトでモデルを使用する前には、モデルにデータをセットする必要がある。
しかしActivity、FragmentなどのUIを管理するコントローラにそのような処理を書き続けることにはいくつか問題がある。
- UIコントローラが破棄された場合、UIを生成しなおさなければいけない。
(場合によってはデータも取得する必要がある) - 通信で取得した値を表示するなどの場合、コード量が多くなって肥大化してしまう。
- 監視したいデータが変更になった場合、毎回listenerなどを呼び出してUIを再生成する必要がある。
- 一覧-詳細画面など同じ情報を使う画面において、それぞれでデータを作成する必要がある。
そこでViewModelを使用すると、この辺をすっきり書くことができる。
ViewModelはUI関連のデータを保存し管理するためのクラスで、UIコントローラが破棄された場合でもデータを保持しておくことができる。(例えば、画面を回転したときなど。)
ViewModelを導入する前に、activity-ktxを追加しておく
こちらもAndroidJetpackの一部で、androidxのActivityコンポーネントを使えるようにする。
dependencies {
def activity_version = "1.1.0"
implementation "androidx.activity:activity-ktx:$activity_version"
}
ViewModelを実装する
ViewModelは変更した値を保持するため、このデータは後続のアクティビティや
フラグメントで使用できる。
ViewModelクラスを継承して作成する。
以下では星座占いのデータを持つクラスを実装した。
class MainViewModel : ViewModel() {
lateinit var horoscope: Horoscope
fun setHoroscope(){
// データを設定するなにがしかの処理
horoscope = Horoscope("Aries", "good", "bad", "very good")
}
}
レイアウトでのバインディングは先ほどと同じ。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<data>
<import type="android.view.View"/>
<variable
name="horoscope"
type="com.yayame.databindingapp.model.Horoscope" />
</data>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<TextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{horoscope.sign}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text1" />
ViewModelをActivityから呼び出す
上記で作成したViewModelを呼び出す。
先ほど設定したactivity-ktxを使用すると、"by viewModels()"という記述が可能になる。
生成したviewModelの値をバインディングする。
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// test2
val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
mainViewModel.setHoroscope()
binding.horoscope = mainViewModel.horoscope
}
}
※このandroidxのviewModelsをインポートしたとき、JVM targetが1.8以上でないとエラーになることがある。
その場合は以下の記事が参考になった。
https://qiita.com/kph7mgb/items/28ee37957976e80e38f2
データが変わったときに自動で UI に反映したい -監視可能なデータオブジェクト(Observable)
監視可能とは、オブジェクトがそのデータの変更について他のオブジェクトに通知できることを意味する。
データバインディングライブラリを使用すると、データの変更を監視できるようになる。
従来のオブジェクトは変更しても UI は自動的に更新されないが、監視可能な
- オブジェクト
- フィールド
- コレクション
がUIにバインドされていると、UI も自動更新できる。
例えば通信中にステータスに応じてプログレスバーの表示/非表示などを変えたい場合に使うと便利。
今回は監視可能なフィールドについてのみ取り扱う。
※Android Studio 3.1 以降では、監視可能なフィールドを LiveData に置き換えることができるが今回は割愛する。
監視可能なフィールド
1つのフィールドを持つ監視可能なオブジェクト。
読み取り専用(Java:final/Kotlin:val)で定義する必要がある。
ここでは ViewModel に読み取り専用のメンバ変数として sign を ObservableField 型で定義している。
※使いたいプリミティブな型に応じてObservableFieldのクラスを使い分ける。
https://developer.android.com/topic/libraries/data-binding/observability?hl=ja
class MainViewModel : ViewModel() {
val sign = ObservableField<String>()
fun setHoroscope(){
// データを設定するなにがしかの処理
sign.set("Aries")
}
}
data で ViewModel ごとインポートするように変更
(先ほどまでは data に Horoscope の Model を定義していたが、ObservableFieldを使うことにより
Actionでデータバインドする時にこまごまするため)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<data>
<variable
name="mainViewModel"
type="com.yayame.databindingapp.model.viewModel.MainViewModel" />
</data>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<TextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{mainViewModel.sign}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text1" />
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
データのセットをした後、ViewModel ごとバインディングする。
これで ViewModel の ObservableField に変更があったとき、UI に反映されるようになった。
実際にtext2をクリックして ViewModel の値を変更すると、値が変わることが分かる。
class MainActivity : AppCompatActivity() {
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// test2
val binding: ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
mainViewModel.setHoroscope()
binding.mainViewModel = mainViewModel
// クリックしてUIを変更してみる
findViewById<TextView>(R.id.text2).setOnClickListener {
mainViewModel.sign.set("Gemini")
}
}
}