Android data binding with Kotlin, BaseObservable, and a custom delegate
Asked Answered
S

3

13

I'm attempting to write a custom delegate which would clean up the syntax for databinding in a Kotlin class. It would eliminate the need to define a custom getter and setter for every property I might want to observe.

The standard implementation in Kotlin appears to be as follows:

class Foo : BaseObservable() {

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

Clearly, with a lot of properties this class can become pretty verbose. What I would like instead is to abstract that away into a delegate like so:

class BaseObservableDelegate(val id: Int, private val observable: BaseObservable) {

     @Bindable
     operator fun getValue(thisRef: Any, property: KProperty<*>): Any {
         return thisRef
     }

     operator fun setValue(thisRef: Any, property: KProperty<*>, value: Any) {
         observable.notifyPropertyChanged(id)
     }
}

Then, the class which extends BaseObservable could go back to having one-line variable declarations:

class Foo : BaseObservable() {
      var bar by BaseObservableDelegate(BR.bar, this)
}

The problem is that without the @Bindable annotation in the Foo class, no propertyId is generated in BR for bar. I'm unaware of any other annotation or method for generating that property id.

Any guidance would be appreciated.

Saguaro answered 4/9, 2017 at 2:31 Comment(0)
A
20

You can annotate the default getter or setter without providing a body.

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

There is a shortcut annotation use-site target which does the same thing.

@get:Bindable var bar: String by Delegates.observable("") { prop, old, new ->
    notifyPropertyChanged(BR.bar)
}
Andrewandrewes answered 4/9, 2017 at 6:14 Comment(0)
Z
3

Additionaly to the accepted answer - sometimes you need variables passed in constructor. It is easy to do too.

class Foo(_bar: String) : BaseObservable() {
      @get:Bindable var bar by Delegates.observable(_bar) { _, _, _ ->
          notifyPropertyChanged(BR.bar)
      }
}

Sometimes we have to save object using parcel, I had some problems using delegete, so code looks like this:

@Parcelize
class Foo(private var _bar: String) : BaseObservable(), Parcelable {
    @IgnoredOnParcel
    @get:Bindable var bar 
    get() =  _bar
    set(value) {
        _bar = value
        notifyPropertyChanged(BR.bar)
    }
}
Zanze answered 17/7, 2018 at 10:56 Comment(0)
T
0

I considered using the androidx.databinding.ObservableField wrapper for my fields. However, it was quite annoying having to read the values as field.get() and write them field.set(value) from the Kotlin code. Also, this approach does require special converters for serialization if you are using it with Retrofit or Room Database.

Finally, I came up with the below approach which allows me to define the variable in a single line as oppose to the accepted answer and keep the field to their default type without any wrapper. Thanks to the Kotlins property delegation. Now, I don't have to write converters for the serialization and have all the benefit from databinding.


class ObservableField<T : BaseObservable, V>(initialValue: V, private val fieldId: Int = -1) : ReadWriteProperty<T, V> {
    private var value: V = initialValue

    override fun getValue(thisRef: T, property: KProperty<*>): V {
        return value
    }

    override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
        this.value = value
        if (fieldId == -1) {
            thisRef.notifyChange()
        } else {
            thisRef.notifyPropertyChanged(fieldId)
        }
    }
}

class Credential: BaseObservable() {
    var username: String by ObservableField("")
    @get:Bindable var password: String by ObservableField("", BR.password)
}


Tit answered 20/6, 2022 at 11:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.