LoginSignup
4
5

More than 3 years have passed since last update.

【Android / Kotlin】双方向 DataBinding + Clickイベント / サンプルアプリ実装

Last updated at Posted at 2021-01-24

はじめに

こちらの記事にて Java で同様のサンプルアプリを作成している。
今回はその Kotlin バージョンである。

双方向データバインディングとは(一方向との違い)

  • 一方行データバインディング
    • ViewModel などの中でデータを変更 → View に反映
  • 双方向データバインディング
    • ViewModel などの中でデータを変更 → View に反映
    • ユーザが View でデータを変更 → ViewModel にデータの変更が反映(通知)

一方向が ViewModel などのロジックファイルから View 側への通知だけであるのに対し、
双方向ではユーザがアプリのフォームなどに値を入力したときなどに、ロジックファイルの方にも値の入力(変更)が通知される。と言うものである。

なお一方向データバインディングについては 言語はJavaですがこちらで記事にしております。

※ ここで説明している ViewModel とはデータバインディングのロジックを定義してあるファイルのことを指しています。

サンプルアプリの概要

こんな感じのもの↓↓
無茶苦茶画質悪くてすみません。

実装していることとしては

  • フォーム入力時
    • 入力されたテキストをフォーム下部にリアルタイムで表示
    • ボタンアクティブにする(テキストの有無で切り替え)
  • ボタンクリック時
    • フォーム入力テキストを上部に表示
    • フォームおよび下部のテキストを空欄に戻す(初期化する)

開発環境

  • Android Studio: 4.1.1
  • Build #AI-201.8743.12.41.6953283, built on November 5, 2020
  • Runtime version: 1.8.0_242-release-1644-b3-6915495 x86_64
  • VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
  • macOS: 10.15.7

実装

DataBinding導入

build.gradleに記述を追加して DataBindingを利用できるようにする。
2箇所に記述。
Java と異なり、アノテーションを導入する記述 (kapt) が必要になるので注意が必要。
こちらで kapt導入について の記事も書いてます。

app/build.gradle
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt' // ←ここに追加
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.3"

    defaultConfig {
        applicationId "com.android.bidirectionaldatabindingkotlin"
        minSdkVersion 24
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }

    // ここに記述を追加
    buildFeatures {
        dataBinding = true
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

レイアウト要素をセット

まずは各要素には DataBinding の記述無しの状態で xml にのレイアウトを作成。

<layout></layout>だけは先に記述。
このようにレイアウトのルート要素に <layout>でくくったレイアウトファイルがあると、自動的にxmlファイル名に応じたBindingクラスが作られる。
今回: activity_main.xml => ActivityMainBinding

activity_main.xml
<?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"
    >

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

        <TextView
            android:id="@+id/click_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.15"
            />

        <EditText
            android:id="@+id/input_form"
            android:layout_width="184dp"
            android:layout_height="wrap_content"
            android:hint="テキストを入力"
            android:inputType="text"
            android:textSize="20dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/button"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.3"
            />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            app:layout_constraintStart_toEndOf="@id/input_form"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/input_form"
            />

        <TextView
            android:id="@+id/realTimeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            app:layout_constraintStart_toStartOf="@id/input_form"
            app:layout_constraintTop_toBottomOf="@id/input_form"
            android:layout_marginTop="16dp"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

DataBinding のロジック定義をするViewModelを作成する

ViewModel.kt
// BaseObservableクラスを継承
class ViewModel : BaseObservable() {
// ここにロジックを書いていく
}

MainActivityでViewModelをバインドする(紐づける)

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        title = "双方向データバインディング Kotlin"

        // ViewModel とバインディングする(紐付ける)
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = ViewModel()
    }
}

レイアウトファイルでViewModelオブジェクトを利用できるようにする

レイアウトファイルに記述を追加

activity_main.xml
<?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>

    <!--        viewModelという名前でViewModelオブジェクトを登録  -->
        <variable
            name="viewModel"
            type="com.android.bidirectionaldatabindingkotlin.ViewModel" />
            />

    </data>

<!--   ・・・ 省略 ・・・   -->

準備が整ったのでこれから機能を実装していく。

フォーム入力でボタンアクティブにする

ViewModel

以下を記述

  • @get:Bindable ~ set(value)
  • isButtonEnable()
ViewModel.kt
class ViewModel : BaseObservable() {

    // これでフォーム入力内容(formText)の getterとsetter が同時にセットできる
    @get:Bindable
    var formText: String = ""
        set(value) {
            // fieldはformTextのこと
            // フォーム入力内容がformTextにセットされる
            field = value
            // View側にbuttonEnableの変更を通知(isButtonEnable()を呼ぶ)
            notifyPropertyChanged(BR.buttonEnable)
        }

    // フォーム(EditText)へのテキスト入力有無で、ボタン活性・非活性を制御するフラグの getter
    @Bindable fun isButtonEnable(): Boolean {
        // 入力あり:true  入力なし:false
        return !formText.isNullOrBlank()
    }
}

補足

Java に 比べると Kotlin は getter と setter を両方定義する際かなりシンプルに記述できる。

ViewModel.kt

    @get:Bindable
        var formText: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.buttonEnable)
            }

↑↑ は ↓↓ こう書いたのと同じである。

ViewModel.kt
    private var formText = ""

    @Bindable fun getFormText(): String {
        return formText
    }

    fun setFormText(formText: String) {
        this.formText = formText
        notifyPropertyChanged(BR.buttonEnable)
    }

レイアウト

以下を記述

  • フォーム要素にandroid:text="@={viewModel.formText}"
  • ボタン要素にandroid:enabled="@{viewModel.buttonEnable}"
activity_main.xml
<!--  ・・・ 省略 ・・・ -->

        <!--  「@={}」で双方向のバインディング -->
        <!--  ViewModelの formText定義 @get:Bindable~set(value) に対応 -->
        <EditText
            android:id="@+id/input_form"
            android:layout_width="184dp"
            android:layout_height="wrap_content"
            android:hint="テキストを入力"
            android:inputType="text"
            android:text="@={viewModel.formText}" ←ここを追加
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/button"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.3"
            />

        <!-- ViewModelの isButtonEnable() に対応 -->
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            android:enabled="@{viewModel.buttonEnable}" ←ここを追加
            app:layout_constraintStart_toEndOf="@id/input_form"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/input_form"
            />

<!--  ・・・ 省略 ・・・ -->

処理の流れ

  1. View側でフォームの値を変更(入力または削除)したときにViewModelformTextset()処理が呼ばれる。
  2. このタイミング(formTextが変更されたタイミング)でnotifyPropertyChanged(BR.buttonEnable)によりisButtonEnable()を呼ぶ。
  3. isButtonEnable()formTextの値があるかどうかによりbooleanを返す
  4. View側android:enabled="@{viewModel.buttonEnable}"booleanが入ってくることによりボタンの活性非活性が反映される

ポイント

@={viewModel.formText}により双方向データバインディングが実現される。つまりView側でフォームの値を変更(入力または削除)したときにViewModelformTextset()が呼ばれるようになる。
@=ではなく@だと一方向のデータバインディングとなり、フォーム入力内容の変更が検知されずset()が呼ばれないため注意。

フォーム入力内容をリアルタイム表示させる

ViewModel

以下を追加

  • formTextset()内にnotifyPropertyChanged(BR.realTimeText);
  • getRealTimeText()
ViewModel.kt
class ViewModel : BaseObservable() {

    // これでフォーム入力内容(formText)の getterとsetter が同時にセットできる
    @get:Bindable
    var formText: String = ""
        set(value) {
            // fieldはformTextのこと
            // フォーム入力内容がformTextにセットされる
            field = value
            // View側にrealTimeTextの変更を通知(getRealTimeText()を呼ぶ)
            notifyPropertyChanged(BR.realTimeText)
            // View側にbuttonEnableの変更を通知(isButtonEnable()を呼ぶ)
            notifyPropertyChanged(BR.buttonEnable)
        }

    // フォーム入力内容をフォーム下のTextViewに反映する getter
    @Bindable fun getRealTimeText(): String {
        // return formText でも良いがわかりやすく?一旦変数に代入してから return している
        val realTimeText = formText
        return realTimeText
    }

    // フォーム(EditText)へのテキスト入力有無で、ボタン活性・非活性を制御するフラグの getter
    @Bindable fun isButtonEnable(): Boolean {
        // 入力あり:true  入力なし:false
        return !formText.isNullOrBlank()
    }
}

レイアウト

フォーム入力の内容をリアルタイムで表示したいTextViewにandroid:text="@{viewModel.realTimeText}"を記述

activity_main.xml
        <!-- 一部抜粋して記述 -->

        <!-- ViewModelの getRealTimeText() に対応 -->
        <TextView
            android:id="@+id/realTimeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.realTimeText}"  ←ここを追加
            android:textSize="20dp"
            app:layout_constraintStart_toStartOf="@id/input_form"
            app:layout_constraintTop_toBottomOf="@id/input_form"
            android:layout_marginTop="16dp"
            />

処理の流れ

  1. フォーム入力内容の変更でformTextset()内のnotifyPropertyChanged(BR.realTimeText);によりgetRealTimeText()が呼ばれる。
  2. getRealTimeText()の返り値がレイアウトのandroid:text="@{viewModel.realTimeText}"`に取得され表示される。

ポイント

notifyPropertyChanged(BR.~)によりプロパティの変更を通知できる。
getter@Bindable、もしくは@get:Bindableを付与する。そうすることでモジュールパッケージ内のBRクラスにデータバインディングで使用するリソースIDを持つ監視変数が作成されるため、このような記述が可能になる。

詳しくはこちらを参照

BRクラスへ監視変数作成例

以下二つを記述

  • @get:Bindable var formText ~ set(value)
  • @Bindable fun isButtonEnable(): Boolean
BR.java

public class BR {

  public static final int formText = 1;

  public static final int buttonEnable = 2;
}

ボタンクリックで テキスト表示+フォーム初期化 をさせる

ViewModel

以下を追加

  • 変数clickText
  • getClickText()
  • onButtonClick()
ViewModel.kt
class ViewModel : BaseObservable() {
    private var clickText: String = "ボタンクリックでここに表示"

    // これでフォーム入力内容(formText)の getterとsetter が同時にセットできる
    @get:Bindable
    var formText: String = ""
        set(value) {
            // fieldはformTextのこと
            // フォーム入力内容がformTextにセットされる
            field = value
            // View側にrealTimeTextの変更を通知(getRealTimeText()を呼ぶ)
            notifyPropertyChanged(BR.realTimeText)
            // View側にbuttonEnableの変更を通知(isButtonEnable()を呼ぶ)
            notifyPropertyChanged(BR.buttonEnable)
        }

     // ボタンクリック時に表示するテキスト(TextView)の getter
     @Bindable fun getClickText(): String {
         return clickText
     }

     // フォーム入力内容をフォーム下のTextViewに反映する getter
     @Bindable fun getRealTimeText(): String {
         // return formText でも良いがわかりやすく?一旦変数に代入してから return している
         val realTimeText = formText
         return realTimeText
     }

     // フォーム(EditText)へのテキスト入力有無で、ボタン活性・非活性を制御するフラグの getter
     @Bindable fun isButtonEnable(): Boolean {
         // 入力あり:true  入力なし:false
         return !formText.isNullOrBlank()
     }

    // ボタンクリックイベント
    fun onButtonClick() {
        // clickTextにフォーム入力テキストをセット
        clickText = formText
        // formTextを初期化
        formText = ""
        // 変更を通知
        // この記述でgetClickText()が呼ばれる
        notifyPropertyChanged(BR.clickText)
        // この記述でgetFormText()が呼ばれる
        notifyPropertyChanged(BR.formText)
    }
}

レイアウト

以下を追加

  • ボタンクリック時表示したいテキストにandroid:text="@{viewModel.clickText}"
  • ボタンにandroid:onClick="@{() -> viewModel.onButtonClick()}"
activity_main.xml
        <!-- クリック時表示テキストを抜粋 -->

        <!-- ViewModel の getClickText() に対応 -->
        <TextView
            android:id="@+id/click_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:text="@{viewModel.clickText}"  ←ここを追加
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.15"
            />

        <!-- ボタン部分を抜粋 -->

        <!-- ViewModel の onButtonClick() に対応 -->
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            android:enabled="@{viewModel.buttonEnable}"
            android:onClick="@{() -> viewModel.onButtonClick()}" ←ここを追加
            app:layout_constraintStart_toEndOf="@id/input_form"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/input_form"
            />       

ポイント

ボタンクリックしたときの処理の流れ

  1. onButtonClick()が呼ばれる
  2. clickTextにそのときのフォーム入力内容を代入
  3. formText""を代入して初期化
  4. notifyPropertyChanged(BR.clickText/formText)でそれぞれの変数の変更をView側に通知
  5. getClickText()getRealTimeText()が呼ばれ、 View側に変数の値が反映される

補足

Viewが描画される?(アクティビティとバインディングされる?)タイミングでも全てのgetterが呼ばれていた。それによりView側に変数の値が反映される。

レイアウトファイル全体

activity_main.xml
<?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"
    >

    <!--        viewModelという名前でViewModelオブジェクトを登録  -->
    <data>
        <variable
            name="viewModel"
            type="com.android.bidirectionaldatabindingkotlin.ViewModel" />
    </data>

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

        <TextView
            android:id="@+id/click_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:text="@{viewModel.clickText}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.15"
            />

        <EditText
            android:id="@+id/input_form"
            android:layout_width="184dp"
            android:layout_height="wrap_content"
            android:hint="テキストを入力"
            android:inputType="text"
            android:textSize="20dp"
            android:text="@={viewModel.formText}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/button"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintVertical_bias="0.3"
            />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="ボタン"
            android:enabled="@{viewModel.buttonEnable}"
            android:onClick="@{() -> viewModel.onButtonClick()}"
            app:layout_constraintStart_toEndOf="@id/input_form"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/input_form"
            />

        <TextView
            android:id="@+id/realTimeText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20dp"
            android:text="@{viewModel.realTimeText}"
            app:layout_constraintStart_toStartOf="@id/input_form"
            app:layout_constraintTop_toBottomOf="@id/input_form"
            android:layout_marginTop="16dp"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

参考

ありがとうございます!!
非常に参考にさせていただきました!!

最後に

データバインディングの書き方は他にもObservableField()を利用したものなど他にも方法があるようですが、今回はこのような方法で書きました。

今後も Android開発極めるべく、勉強していきます!!

ありがとうございました!!

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5