Effective Kotlin - Properties should represent state, not behavior

Kotlin properties may look similar to Java fields, but they have different concepts.


// Kotlin property
var name: String? = null

// Java field
String name = null;

Even if you use them in the same way, Kotlin properties can do much more.

Full syntax for property declaration


var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

Custom Get, Set (var)


var name: String? = null
    get() = field?.toUpperCase()
    set(value) { if(!value.isNullOrBlank()) { field = value } }

Custom Get (val)


val fullName: String?
    get() = "$name $surname"

Backing Fields

Fields cannot be declared customly. However, Kotlin automatically generates a backing field if the property requires one. This backing field can be accessed in accessors using the `field` identifier.


var counter = 0 // the initializer assigns the backing field directly
    set(value) {
        if (value >= 0)
            field = value
            // counter = value // ERROR StackOverflow: Using actual name 'counter' would make setter recursive
    }

Different from functions?

In Kotlin, if a property represents the state of an object, a function represents the behavior of that object.


// property
val isEmpty: Boolean
  get() = amount == 0


// function
fun isEmpty(): Boolean {
  return amount == 0
}

Let's think about it.

Is it always better to use properties?


class FruitBucket(
    val price: Int,
    val discount: Int,
    var amount: Int
) {
  val isEmpty: Boolean
    get() = amount == 0
  val salePrice: Int
    get() = price - discount
}

The code above can be improved as follows:
Because calculating it every time it is accessed is inefficient.


class FruitBucket(
    val price: Int,
    val discount: Int,
    var amount: Int
) {
  val isEmpty: Boolean
    get() = amount == 0
  val salePrice: Int = price - discount
}

You can use properties without holding data.

For some reason, even if you cannot use the `Date` type, you can use Kotlin's property features to work with `Date` values by using other values instead of storing actual `Date` type data (e.g., when serializing, you might hold only the `millis` value and convert it to `Date` format when used).


var date: Date
    get() = Date(millis)
    set(value) {
        millis = value.time
    }

Properties can be used as accessors, not just as simple fields.


val Context.preferences: SharedPreferences
    get() = PreferenceManager.getDefaultSharedPreferences(this)
val Context.inflater: LayoutInflater
    get() = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val Context.notificationManager: NotificationManager
    get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

Are properties a silver bullet?

Properties that contain specific logic such as loops, recursion, or algorithmic parts are not the correct way to use them.


// Don't do this!
val Tree<Int>.sum: Int
    get() = when (this) {
        is Leaf -> value
        is Node -> left.sum + right.sum
    }

Properties should generally only be used to represent or set state.
Here are examples where using a function is recommended over using a property:

  • When the complexity is higher than O(1) (i.e., calculation is computationally expensive)
  • When it contains business logic beyond simple operations (e.g., logging, updating elements)
  • When calling the member twice in a row produces different results
  • When it duplicates conventional expressions like `Int.toDouble()`
  • When the getter changes the state of the property

References

AD