はじめに
こんにちは。普段は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を愛でていきたいです。