Tatsuro のテックブログ

アプリ開発の技術情報を発信します。

【Android × Kotlin】Data Binding でレイアウトとリスナーメソッドをバインドする

今回は、Data Binding でレイアウトとリスナーメソッドをバインドする方法を解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Electric Eel | 2022.1.1 Patch 1
  • JDK 11.0.15
  • Android Gradle Plugin 7.4.1
  • Kotlin 1.8.0
  • Gradle 7.6.0
  • androidx.lifecycle 2.5.1

概要

前回、Data Binding でレイアウトと StateFlow で保持する状態をバインドする方法を解説しましたが、Data Binding を使ってレイアウトとリスナーメソッドをバインドすることもできます。

例えば、ViewModel に Button がクリックされたときに呼びたいメソッドを定義し、Activity や Fragment などで View#setOnClickListener を呼ぶことなく、レイアウトの実装だけで ViewModel のメソッドをバインドすることができます。

これにより、Activity や Fragment などの実装を減らしながら、View のリスナーメソッドを ViewModel に実装することができます。

今回は、Button と Switch のリスナーメソッドの実装を通して、Data Binding でレイアウトとリスナーメソッドをバインドする方法を解説します。

ライブラリのインポート

Data Binding を有効にする場合、アプリの build.gradle ファイルの android ブロックに以下の設定を追加します。

android {
    buildFeatures {
        dataBinding true
    }
}

また、今回の解説では ViewModel も使用するため、以下の依存関係を追加します。

dependencies {
    // AndroidX Lifecycle
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
}

以上で、ライブラリのインポートは完了です。

実装

Data Binding でリスナーメソッドをバインドする場合、メソッド参照とリスナーバインディングの 2 つの方法があります。それぞれについて、解説します。

メソッド参照

メソッド参照でレイアウトとリスナーメソッドをバインドする場合、バインド先の View のリスナーメソッドと実装するリスナーメソッドのメソッドシグネチャを完全に一致させる必要があります。

まず、ViewModel を作成して、その ViewModel にリスナーメソッドを実装します。

class MainViewModel : ViewModel() {

    fun onButtonClick(
        @Suppress("UNUSED_PARAMETER") view: View
    ) {
        Log.d("MainViewModel", "Button clicked.")
    }

    fun onSwitchCheckedChanged(
        @Suppress("UNUSED_PARAMETER") buttonView: CompoundButton,
        @Suppress("UNUSED_PARAMETER") isChecked: Boolean
    ) {
        Log.d("MainViewModel", "Switch checked changed.")
    }
}

Button のクリックリスナーは引数に View を持つため、ViewModel のリスナーメソッドもそれに合わせます。Switch のチェック変更リスナーも同様に引数を合わせます。

そして、レイアウトは以下のように実装します。

<?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"
    tools:context=".MainActivity">

    <data>

        <variable
            name="viewModel"
            type="com.tatsuro.app.databindinglistenermethod.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/sampleButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:onClick="@{viewModel::onButtonClick}"
            android:text="Button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.switchmaterial.SwitchMaterial
            android:id="@+id/sampleSwitch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onCheckedChanged="@{viewModel::onSwitchCheckedChanged}"
            android:text="Switch"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/sampleButton" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

リスナーを設定する属性に、@{} 構文とメソッド参照を組み合わせて設定します。

以上で、View でイベントが発生したときにメソッド参照でバインドされた ViewModel のリスナーメソッドが呼ばれます。

リスナーバインディング

メソッド参照と異なり、リスナーバインディングではメソッドの引数が一致する必要はなく、戻り値だけが一致していればよいです。

まず、ViewModel を作成して、その ViewModel にリスナーメソッドを実装します。

class MainViewModel : ViewModel() {

    fun onButtonClick() {
        Log.d("MainViewModel", "Button clicked.")
    }

    fun onSwitchCheckedChanged() {
        Log.d("MainViewModel", "Switch checked changed.")
    }
}

リスナーメソッドは引数なしで実装します。

そして、レイアウトは以下のように実装します。

<?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"
    tools:context=".MainActivity">

    <data>

        <variable
            name="viewModel"
            type="com.tatsuro.app.databindinglistenermathod2.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <Button
            android:id="@+id/sampleButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:onClick="@{() -> viewModel.onButtonClick()}"
            android:text="Button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.switchmaterial.SwitchMaterial
            android:id="@+id/sampleSwitch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onCheckedChanged="@{() -> viewModel.onSwitchCheckedChanged()}"
            android:text="Switch"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/sampleButton" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

リスナーバインディングの場合、@{} 構文とラムダ式を組み合わせて設定します。

以上で、View でイベントが発生したときにリスナーバインディングでバインドされた ViewModel のリスナーメソッドが呼ばれます。

参考

レイアウトとバインディング式 - イベント処理

関連記事

Data Binding でレイアウトと StateFlow をバインドする方法については、以下で解説しています。

tatsurotech.hatenablog.com

ViewModel の使用方法については、以下で解説しています。

tatsurotech.hatenablog.com