Data BindingはViewシステムを用いている場合に、Viewの表示と状態(データ)を結合する仕組みです。
結合とは「状態の変化に連動して、表示を更新する」ことです。
Googleは「利点がある」と述べています。しかし、私は利点に感じないので、積極的な利用をしていません。
Googleが提供するAndroidアプリのサンプルで頻繁に登場し、よく見かけます。
ですので、ここに備忘録として、まとめます。
※環境:Android Studio Ladybug Feature Drop | 2024.2.2
Kotlin 2.0.0
(Viewシステムのプロジェクトは1.9.24が選ばれる)
(Composeのプロジェクトは2.0.0が選ばれる)
目次
Data Vindingとは
Data BindingはViewシステム(レイアウトファイルで画面構成を定義する環境)を用いている場合に、Viewの表示と状態(データ)を結合する仕組みです。
結合とは「状態の変化に連動して、表示を更新する」ことです。
AAC(Androidアーキテクチャコンポーネント)の「UIの分離」を意図したプログラミング手法です。
Data Bindingあり・なしを比較すれば、違いと利点がわかると思います。
Data Bindingなし(UIとロジックの関係が密)
表示を更新する場合、ロジック側は対象のViewを把握して、そのView固有のセッター関数で、新たな状態を設定する必要があります。
ロジック側でfindViewByIdを用いて、Viewのインスタンスを取得するのは、そのためです。
UIとロジックの関係はとても「密(みつ)」になります。
しかも、Viewの操作はメインスレッドで行う必要があります。
Data BindingなしData Bindingあり(UIとロジックの関係が疎)
状態を保持する変数はUI側に持ちます。状態はViewと結合されていて、状態の変化が直ちにViewの表示に反映されます。
表示を更新する場合、ロジック側は状態を保持する変数へ、新たな状態を書き込むだけです。対象のViewを把握する必要はありません。
UIとロジックの関係は「疎(そ)」になります。
しかも、Viewの操作を行うわけでは無いので、状態の書き込みはワーカースレッドで行うことができます。
Data Bindingあり環境設定
Data Bindingを利用するための環境設定は、以下の3つが必要です。
- (1)Data Bindingの有効化
- (2)Viewシステムの採用
- (3)レイアウトファイルの変換
特に「(3)レイアウトファイルの変換」を行わないと、(1)と(2)が出来ていても、機能(Bindingの動作)は無反応です。注意して下さい。
Data Bindingの有効化
Data Bindingはモジュール毎に有効・無効を指定します。
有効化の設定
有効にするために、「app/build.gradle」へ次の設定を行います。
plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) id("org.jetbrains.kotlin.kapt") } android { ... buildFeatures { dataBinding = true } ... }
Data BindingはKapt(Kotlin Anotation processing tools)を使います。Ksp(Kotlin Symbole Processor)はサポートしていないので、使えません。
Kotlin 2の対応
現在(2025.01)、Kotlinは1.9.xから2.0.xへ移行を果たしています。それに伴い、KaptもKspへ移行することが推奨されています。KaptはKotlin 1.9.x世代のプラグインなのです。
ですが、下記の設定をgradele.propertiesへ行うと、KaptはKotlin 2をサポートします。
: : # KapeのKotlin 2対応を有効化 kapt.use.k2=true
ただし、「サポートは実験段階です。評価目的のみに使用してください。」とドキュメントに書かれています。
上記の事項が承諾できないならば、Kotlinを1.9.xに戻してData Bindingをも用いる必要があります。
ちなみに、最新版のKaptはメンテナンス モードという位置づけでリリースされています。今後、新機能を実装する予定はなく、KotlinやJavaのバージョンアップに対応するのみになるようです。「いつまで対応が続くか?!」は不明です。
Viewシステムの採用
Data BindingはViewシステムを用いている場合の機能です。
現状はJetpack Composeによる開発が主流になっているので、Viewシステムを意図的に選択しなければなりません。
新規プロジェクトの作成時に、「Views Activity」を選んでください。
レイアウトファイルの変換
レイアウトファイル(XXX.xml)をData Binding対応へ変換します。
<layout>タグで全体を囲み、レイアウトファイル中のルートViewと並列に<data>タグを設けてください。
<?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> <!-- ここにBinding変数を定義 --> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textAllCaps="false" android:textSize="48sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" android:textAllCaps="false" android:textSize="48sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
この変換作業ですが、Android Studioに自動変換する機能が付いています。こちらを使うと簡単です。
Binding動作
Data Bindingの有効化が行われると、Android Studioはレイアウトファイルを認識する際に、Bindingクラス(例:ActivityMainBinding)を自動生成します。
Bindingクラスのクラス名は、レイアウトファイル名(例:activity_main.xml)を図のように変換したものになります。
Bindingクラス#infulate()を実行すれば、BindingクラスのプロパティにBinding変数のインスタンスが登録されます。※方法2のやり方もあります。
そして、そのプロパティを使って、Binding変数へアクセスできるようになります。
binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // トップViewをコンテンツとして指定 binding.message = "Hello Android!" // Binding変数(message)の参照 binding.color = Color.WHITE // Binding変数(color)の参照
// DataBindingUtilを用いた方法 binding = DataBindingUtil.setContentView(this, R.layout.activity_main) binding.message = "Hello Android!" // Binding変数(message)の参照 binding.color = Color.WHITE // Binding変数(color)の参照
状態(データ)の結合
Binding変数は<data>タグ内の<variable>タグで定義します。「name」が変数名、「type」が変数の型です。
Binding変数はロジック側からのアクセスが監視され、書き込みが行われたらViewの関数を実行するように、Viewシステムが制御します。
Binding変数と実行するViewの関数は、図のように関連付けられます。
右辺がBinding式(”@{“と”}”で囲まれた式)の場合は、左辺をViewの関数として扱います。その他の場合はViewの属性として扱います。
Viewの関数は図のようにBinnding式の評価結果を引数にして実行されます。
Binding式は構文があるので、詳細は「レイアウトとバインディング式/式言語」を参照してください。
この時、レイアウト中に記載する関数名は省略形になっています。省略形のルールは図の通りです。
プリミティブ型
Binding変数がプリミティブ型である場合の例です。
<?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> <variable name="message" type="String" /> <variable name="color" type="Integer" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{message}" android:textAllCaps="false" android:textColor="@{color}" android:textSize="34sp" ... /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.message = "Hello Android!" binding.color = Color.WHITE // 別スレッドで5秒後にWHITE->GREENへ変更 lifecycleScope.launch(Dispatchers.Default) { delay(5000) // 5秒待つ binding.color = Color.GREEN // WHITE->RED } }
非同期(5秒後)にWHITE⇒GREENへ変更しています。非同期であってもData Bindingは問題なく動作します。
一般的なクラス
Binding変数が一般的なクラスである場合の例です。
型はフルパス(パッケージ名全体)を書かなければなりません。
data class Message ( var text: String = "Hello Android !", @ColorInt var color: Int = Color.WHITE )
<?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> <variable name="message" type="com.example.sample.Message" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{message.text}" android:textAllCaps="false" android:textColor="@{message.color}" android:textSize="34sp" ... /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.message = Message(color = Color.WHITE) // 別スレッドで5秒後にWHITE->GREENへ変更 lifecycleScope.launch(Dispatchers.Default) { delay(5000) // 5秒待つ binding.message = Message(color = Color.GREEN) // WHITE->GREEN(OK) } }
非同期(5秒後)にWHITE⇒GREENに変更しています。非同期であってもData Bindingは問題なく動作します。
実行結果は「プリミティブ型」と同じです。省略します。
Binding変数の書き込みの有無により、表示が変更される場合とされない場合があるので注意が必要です。
lifecycleScope.launch(Dispatchers.Default) { delay(5000) // 5秒待つ binding.message?.let { // WHITE->GREEN(NG) it.color = Color.GREEN } }
lifecycleScope.launch(Dispatchers.Default) { delay(5000) // 5秒待つ val _message = binding.message?.apply { // WHITE->GREEN(OK) color = Color.GREEN } binding.message = _message }
ちなみに、Binding変数はNullalbe(Null許容型)で、初期値はNullです。
※Binding式でNullの場合の代替値が指定できます(”??”を使う、Binding式の構文のドキュメント参照)。
イベントの結合
イベントハンドラー(実行される関数)を関数オブジェクトの形でリスナーへ登録します(setOnClockListener関数などを用いる)。
ただし、Binding変数の型に関数オブジェクトの型は指定できません。ですので、イベントハンドラーが属するクラスを型にします。
class Handlers { fun clickedMesg(v: View) { // ⇐ イベントハンドラー // // イベントが発生した時に実行する処理 // } }
Viewのリスナーへ登録する関数(setOnClockListener関数など)は、図のようにBinnding式の評価結果を引数にして実行されます。
Binding式が「クラスに属する関数オブジェクト」になっている点がポイントです。
この時、レイアウト中に記載する関数名は省略形になっています。省略形のルールは図の通りです。
イベントハンドラー(関数オブジェクト)の表現方法は、次の2つがあります。
- (1)メソッド参照
- (2)リスナーバインディング
- ※(1)は上記の図に出てきた表現方法
メソッド参照
「メソッド参照」は関数名で関数オブジェクトを表現する方法です。
このサンプルはハンドラーをMainActivityに実装しています。ですので、Binding変数の型はMainActivityです。
<?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> <variable name="message" type="com.example.sample.Message" /> <variable name="activity" type="com.example.sample.MainActivity" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".BasicActivity"> <LinearLayout ...> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{message.text}" android:textColor="@{message.color}" android:onClick="@{activity::clickedMesg}" android:textAllCaps="false" android:textSize="34sp" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.message = Message(color = Color.WHITE) binding.activity = this } fun clickedMesg() { binding.message = binding.message?.apply { color = Color.YELLOW } } }
クリックでWHITE⇒YELLOWへ変更しています。
クリックイベントは引数にViewを持ったハンドラーを起動しようとします。
ですので、ハンドラーの引数にViewが必要です。また、ハンドラーはView以外の引数を持てません。
起動する側とされる側で、引数の整合性が必要だからです。
リスナーバインディング
「リスナーバインディング」はラムダ式で関数オブジェクトを表現する方法です。そのラムダ式から、ハンドラーを名指しで起動します。
このサンプルはハンドラーをMainActivityに実装しています。ですので、Binding変数の型はMainActivityです。
<?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> <variable name="message" type="com.example.sample.Message" /> <variable name="activity" type="com.example.sample.MainActivity" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".BasicActivity"> <LinearLayout ...> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{message.text}" android:textColor="@{message.color}" android:onClick="@{() -> activity.clickedMesg()}" android:textAllCaps="false" android:textSize="34sp" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.message = Message(color = Color.WHITE) binding.activity = this } fun clickedMesg() { binding.message = binding.message?.apply { color = Color.YELLOW } } }
<?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> <variable name="message" type="com.example.sample.Message" /> <variable name="activity" type="com.example.sample.MainActivity" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".BasicActivity"> <LinearLayout ...> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{message.text}" android:textColor="@{message.color}" android:onClick="@{() -> activity.clickedMesg(message)}" android:textAllCaps="false" android:textSize="34sp" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.message = Message(color = Color.WHITE) binding.activity = this } fun clickedMesg(m: Message) { binding.message = binding.message?.apply { color = Color.YELLOW } // mを使った処理 } }
<?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> <variable name="message" type="com.example.sample.Message" /> <variable name="activity" type="com.example.sample.MainActivity" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".BasicActivity"> <LinearLayout ...> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{message.text}" android:textColor="@{message.color}" android:onClick="@{(view) -> activity.clickedMesg(view)}" android:textAllCaps="false" android:textSize="34sp" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.message = Message(color = Color.WHITE) binding.activity = this } fun clickedMesg(v: View) { binding.message = binding.message?.apply { color = Color.YELLOW } // vを使った処理 } }
<?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> <variable name="message" type="com.example.sample.Message" /> <variable name="activity" type="com.example.sample.MainActivity" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".BasicActivity"> <LinearLayout ...> <TextView android:id="@+id/txtMesg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{message.text}" android:textColor="@{message.color}" android:onClick="@{(view) -> activity.clickedMesg(view, message)}" android:textAllCaps="false" android:textSize="34sp" /> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.message = Message(color = Color.WHITE) binding.activity = this } fun clickedMesg(v: View, m: Message) { binding.message = binding.message?.apply { color = Color.YELLOW } // mとvを使った処理 } }
クリックでWHITE⇒YELLOWへ変更しています。
実行結果は「プリミティブ型」と同じです。省略します。
ラムダ式からハンドラーを名指しで起動する際に、ハンドラーへView以外の引数を設けることが可能です。ただし、引数にできるのはBinding変数です。
これが、この表現方法の利点になります。
関連記事: