3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

KotlinのDelegated Propertyを使って、Android DatabindingのBaseObservableを実装する

Last updated at Posted at 2019-07-11

はじめに

こんにちは。普段はAndroidアプリの開発をしているのですが、最近KotlinのDelegated Propertyを使って実装することが増えてきました。
また、Android Databindingというデータバインディング用の公式のライブラリがありますが、バインディングの実装方法としてBaseObservableというクラスを継承して実装する方法があります。
そこで今回は、こちらのBaseObservableとKotlinのDelegated Propertyを組み合わせた実装方法が可読性的に良いなと感じたのでその方法について紹介していこうと思います。
間違っている部分あれば指摘していただけますと幸いです。

こちらを参考にしております。
https://stackoverflow.com/questions/46029570/android-data-binding-with-kotlin-baseobservable-and-a-custom-delegate

検証済みの動作環境

以下の環境で正常に動作することを確認しています。

  • Android Studio 3.4.1, 3.5 beta
  • Kotlin 1.3.11

KotlinのDelegated Property

Kotlinの言語機能としてDelegated Propertyというものがあります。日本語に訳すと「委譲プロパティ」となります。
こちらはKotlinのプロパティのset,getする際のロジックを別のクラスとして切り出し、そちらに処理を委譲させることができるものになります。
Delegated Propertyを使った実装の例はこちらになります。byキーワードを使って、クラスを記述します。こちらのクラスには、varであればgetValue()とsetValue()を、valであればgetValue()をそれぞれoperator funtionとして定義しておくことが必要になります。


class Example {
  var p: String by ReadWriteDelegate()
  val q: String by ReadOnlyDelegate()
}

// var用のDelegate用のクラス
class ReadWriteDelegate {
  // pのプロパティの値を取得時にこちらの処理を経由する
  operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    return "$thisRef, thank you for delegating '${property.name}' to me!"
  }

  // pのプロパティの値をセット時にこちらの処理を経由する
  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
    println("$value has been assigned to '${property.name} in $thisRef.'")
  }
}

// val用のDelegate用のクラス
class ReadOnlyDelegate {
  // qのプロパティの値をセット時にこちらの処理を経由する
  operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    return "$thisRef, thank you for delegating '${property.name}' to me!"
  }
}

また、このDelegateに使うインターフェースも用意されています。val用のReadOnlyProperty、var用のReadWritePropertyの2つが用意されているので、プロパティごとに使い分けることになると思います。

// ReadWritePropertyインターフェースを継承する
class ReadWriteDelegate : ReadWriteProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name} in $thisRef.'")
    }
}

// ReadOnlyPropertyインターフェースを継承する
class ReadOnlyDelegate : ReadOnlyProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }
}

これらのコードを以下のように実行するとこうなります。参照するときにはgetValueに記述していた処理が、更新するときはsetValueに記述していた処理が実行されていることがわかると思います。

fun main() {
    val example = Example()
    println(example.p)
    println(example.q)
    example.p = "updated p"
}

↓出力結果

Example@37f8bb67, thank you for delegating 'p' to me!
Example@37f8bb67, thank you for delegating 'q' to me!
updated p has been assigned to 'p in Example@37f8bb67.'

Process finished with exit code 0

Andriod Databindingについて

次はDatabindingについて軽く説明します。Android DatabindingはAndroidにおいてデータバインディングを実現するための公式のライブラリになります。bindするオブジェクトをxmlに定義しておくと、アノテーションプロセッシングによって、Bindingクラスが生成され、xml内で@{}のキーワードの中でbindされたオブジェクトを参照することができるというものです。

詳しくは、こちらを御覧ください。
https://developer.android.com/topic/libraries/data-binding?hl=ja

BaseOvservableによる実装

Android Databindingでは、バインディングする方法として2種類の方法があります。

  • ObservableFieldを使う
  • BaseObservableを使う

今回ObservableFieldでの方法については割愛し、BaseObservableを使った方法を紹介します。BaseObservableを使う方法では、Viewにバインディングさせる対象のプロパティは、以下のようにBaseObservableというクラス内で定義しておき、@Bindable(※Kotlinでは@get:Bindable)アノテーションを付与する必要があります。また、API通信などによって取得したデータによって、バインディングしているプロパティを更新する場合、notifyPropertyChangedメソッドを使って、プロパティが更新されたことを明示的に通知する必要があります。


class UserModel : BaseObservable() {
    @get:Bindable
    var firstName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.firstName)
        }

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

また、アプリの規模が大きくなってきたりすると、UserModelのデータを別のdataクラスとして持たせてそちらを経由させてデータを取得したい場合もあると思います。その場合は、以下のようになり、getterまで記述する必要が出てきました。


class UserModel(val userData: UserData) : BaseObservable() {
    @get:Bindable
    var firstName: String = ""
        get() = userData.firstName
        set(value) {
            userData.firstName = value
            notifyPropertyChanged(BR.firstName)
        }

    @get:Bindable
    var lastName: String = ""
        get() = userData.lastName
        set(value) {
            userData.lastName = value
            notifyPropertyChanged(BR.lastName)
        }
}

data class UserData(val firstName: String, 
                    val lastName: String)

上記のように定義されたクラスは、xml内では@{user.firstName}のように記述することで、モデルクラスのプロパティをlayoutにbindすることができます。


<?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="user"
            type="youmeee.co.jp.app.UserModel" />
    </data>

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

        <TextView
            android:id="@+id/firstName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.firstName}"/>

        <TextView
            android:id="@+id/lastName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.lastName}"/>
        
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

DelegateクラスでBaseObservableを実装する

本題に入ります。上記のようにBaseObservableを使ったクラスはただプロパティをbindするだけであればそこまで冗長になりませんが、参照する際に少しロジックが入ってくると、見通しが悪くなる可能性があります。
また、データの取得時に別のdataクラスを経由したり、bindする前になんらかのロジックが必要な場合があります。その際、getterとsetterを両方記述する必要があり、プロパティが多くあるViewModelをbindする場合などにおいては、毎回それぞれを書かなくてはならず、少し手間がかかりますし、ボイラープレートな記述も増えていきます。
これを解決する方法として、KotlinのDelegated Propertyが使えます。

以下のようにDelegateクラスを定義してみます。


class UserPropertyDelegate(data: String, id: Int) : ReadWriteProperty<Any?, String> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return data
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        data = value
        notifyPropertyChanged(id)
    }
}

こちらを使うと、以下のように、バインディングするプロパティを記述することができます。
setterとgetterをそれぞれ定義する必要がなくなりましたし、notifyPropertyChangedを各プロパティごとに呼ぶ必要もなくなりました。なんて便利!!


class UserModel(val userData: UserData) : BaseObservable() {
    @get:Bindable
    var firstName: String by UserPropertyDelegate(userData.firstName, BR.firstName)

    @get:Bindable
    var lastName: String by UserPropertyDelegate(userData.lastName, BR.lastName)

    /* Before
      @get:Bindable
      var firstName: String = ""
          set(value) {
              field = value
              notifyPropertyChanged(BR.firstName)
          }

      @get:Bindable
      var lastName: String = ""
          set(value) {
              field = value
              notifyPropertyChanged(BR.lastName)
          }
     */
}

また、上記のプロパティでは、Stringのプロパティにしか対応していないので、ジェネリクスを使って汎用的なDelegateクラスにすることも可能です。

// ジェネリクスTを使って、汎用的にDelegateクラスを利用する
class UserPropertyDelegate<T> (data: T, id: Int) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return data
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        data = value
        notifyPropertyChanged(id)
    }
}

Delegates.observableも使える

↑こちらでも紹介されていますが、Delegates.observable()というメソッドを使ってDelegated Propertyを実装する方法もあります。

Delegates.observable()は、以下のような仕様になります。
第一引数: プロパティの初期値
第二引数: プロパティが変更されたときの通知を受け取るラムダ。パラメータとして「割り当てられているプロパティ」「古い値」「新しい値」があります。。

例えば、データバインディング用のプロパティを定義し、setしたあとnotifyPropertyChanged()を呼びたいときは、以下のように記述することができます。プロパティが変更されるたびに、ラムダにてイベントを受け取れるので、ここでnotifyPropertyChanged()を呼ぶことができます。


class UserModel : BaseObservable() {
    @get:Bindable
    var firstName: String by Delegates.observable("") { prop, old, new ->
        notifyPropertyChanged(BR.firstName)
    }
}

まとめ

KotlinのDelegated Propertyの機能を使えばDatabindingのプロパティをシンプルに定義することができました。
これからもKotlinを愛でていきたいです。

参考資料

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?