22

I want to detect any values changed of a property of my class so then I could do another operation after that. In other word, if one of a specific data of a property is changed, then a specific event will fire by then. Actually, if it is a normal class in another programming languages such as Java, then I think I could use setter to do this job after data have modified or use delegate in C#. But since Kotlin is very new, I could not find any solution out there at all. I have tried to overload property but was not successful by any chance. I wanted to use interface to to this too, but again since it is data class, I got no idea how should it be done.

Below is the sample class. In such situation, how to detect when Age or name is changed?

data class Person(var Name: String, var Age: Int) 

So please if anyone have any ideas about this, please help.

Note: in my situation, data class must be used.

jujuzi
  • 389
  • 1
  • 4
  • 16

2 Answers2

28

Data classes are really not made for this. Since their properties have to be declared in the primary constructor, you can't really add custom behaviour to them.

That said, if you must, you can achieve this by duplicating the properties, and then either using custom setters, or Delegates.observable.

Here's a way to do it with custom setters, here you're gonna be accessing the publicly visible name and age properties, which keep the ones declared in the constructor up to date as well:

data class Person(private var _name: String, private var _age: Int) {

    var name = _name
        set(value) {
            println("Name changed from $name to $value")
            field = value // sets the backing field for `name`
            _name = value // sets the `_name` property declared in the primary ctor
        }

    var age = _age
        set(value) {
            println("Age changed from $age to $value")
            field = value
            _age = value
        }

}

Same idea, with Delegates.observable, which does some of the work for you, here your only overhead is setting the properties declared in the constructor to the new values:

data class Person(private var _name: String, private var _age: Int) {

    var name: String by Delegates.observable(_name) { prop, old, new ->
        println("Name changed from $old to $new")
        _name = new
    }

    var age: Int by Delegates.observable(_age) { prop, old, new ->
        println("Age changed from $old to $new")
        _age = new
    }

}

Usage of either of these looks like this (the toString will look a bit ugly with the underscores):

val sally = Person("Sally", 50)
println(sally)      // Person(_name=Sally, _age=50)
sally.age = 51      // Age changed from 50 to 51
println(sally)      // Person(_name=Sally, _age=51)
println(sally.name) // Sally
println(sally.age)  // 51

Edit to answer the question below:

If you didn't need your class to be a data class, the following would probably be the simplest solution:

class Person(name: String, age: Int) {

    var name: String by Delegates.observable(name) { _, old, new ->
        println("Name changed from $old to $new")
    }

    var age: Int by Delegates.observable(age) { _, old, new ->
        println("Age changed from $old to $new")
    }

}

This way you still have a constructor that takes the name and age as parameters, but they get assigned to the properties that are inside the body of the class. This is not possible with a data class, because every constructor parameter of a data class has to be a property as well (marked val or var). For more, you can see the documentation about constructors, properties, and data classes.

zsmb13
  • 85,752
  • 11
  • 221
  • 226
  • When you say, "Since their properties have to be declared in the primary constructor, you can't really add custom behaviour to them." it makes me even more curious. What will you do if their properties don't have to be declared in the primary constructor? And how about supplying data? Because normally, we supply it through constructor, don't we? – jujuzi Jun 10 '17 at 08:18
  • I've edited my answer with the response, hope it helps. – zsmb13 Jun 10 '17 at 08:24
  • Ok.. it is a good one. But I still curious a little more. Just curious what is the purpose of using Delegate.observable? Why using "var name: String by Delegates.observable(_name) { prop, old, new -> println("Name changed from $old to $new") _name = new }" while I could use "var name : String = "" set(value) { field = value} }" – jujuzi Jun 10 '17 at 09:02
  • The two are basically equivalent. The only difference is that with using the delegate, the field gets set automatically, and the only code you need to write is whatever you want to do when the change is happening. You're basically saving the `field = value` line. And you also get the old and new values as parameters, which is a bit nicer perhaps. – zsmb13 Jun 10 '17 at 09:07
  • Could you please tell can I apply Delegates observable to the object of custom type defined by me? Not to primitive type like String? – Liker777 Oct 16 '21 at 14:46
  • Yeah, that makes data classes very inconvenient. If we use setters with underscore in the name like "_name", Firebase syncs with the cloud collection both properties ("name" and "_name"), so finally in the collection we have 2 fields if we follow Firebase manual for data classes. https://firebase.google.com/docs/firestore/manage-data/add-data#custom_objects So it seems like Kotlin data class does not provide an option for changes listener to be used with Firebase :( – Liker777 Oct 20 '21 at 08:56
  • Hi, please tell me that Is it good practice to use private fields in the data class primary constructor? data class Person(private var _name: String, private var _age: Int) – Lokik Soni Oct 30 '21 at 12:17
2

Delegated Properties

There are certain common kinds of properties, that, though we can implement them manually every time we need them, would be very nice to implement once and for all, and put into a library. Examples include

lazy properties: the value gets computed only upon first access, observable properties: listeners get notified about changes to this property, storing properties in a map, instead of a separate field for each property. To cover these (and other) cases, Kotlin supports delegated properties:

class Example {
    var p: String by Delegate()
}

The syntax is: val/var : by . The expression after by is the delegate, because get() (and set()) corresponding to the property will be delegated to its getValue() and setValue() methods. Property delegates don’t have to implement any interface, but they have to provide a getValue() function (and setValue() — for var's). For example:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name} in $thisRef.'")
    }
}
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
Glory
  • 1,007
  • 9
  • 14