Intermediate: Properties
In the beginner tour, you learned how properties are used to declare characteristics of class instances and how to access them. This chapter digs deeper into how properties work in Kotlin and explores other ways that you can use them in your code.
Backing fields
In Kotlin, properties have default get() and set() functions, known as property accessors, which handle retrieving and modifying their values. While these default functions are not explicitly visible in the code, the compiler automatically generates them to manage property access behind the scenes. These accessors use a backing field to store the actual property value.
Backing fields exist if either of the following is true:
You use the default
get()orset()functions for the property.You try to access the property value in code by using the
fieldkeyword.
For example, this code has the category property that has no custom get() or set() functions and therefore uses the default implementations:
Under the hood, this is equivalent to this pseudocode:
In this example:
The
get()function retrieves the property value from the field:"".The
set()function acceptsvalueas a parameter and assigns it to the field, wherevalueis"".
Access to the backing field is useful when you want to add extra logic in your get() or set() functions without causing an infinite loop. For example, you have a Person class with a name property:
You want to ensure that the first letter of the name property is capitalized, so you create a custom set() function that uses the .replaceFirstChar() and .uppercase() extension functions. However, if you refer to the property directly in your set() function, you create an infinite loop and see a StackOverflowError at runtime:
To fix this, you can use the backing field in your set() function instead by referencing it with the field keyword:
Backing fields are also useful when you want to add logging, send notifications when a property value changes, or use additional logic that compares the old and new property values.
For more information, see Backing fields.
Extension properties
Just like extension functions, there are also extension properties. Extension properties allow you to add new properties to existing classes without modifying their source code. However, extension properties in Kotlin do not have backing fields. This means that you need to write the get() and set() functions yourself. Additionally, the lack of a backing field means that they can't hold any state.
To declare an extension property, write the name of the class that you want to extend followed by a . and the name of your property. Just like with normal class properties, you need to declare a type for your property. For example:
Extension properties are most useful when you want a property to contain a computed value without using inheritance. You can think of extension properties working like a function with only one parameter: the receiver.
For example, let's say that you have a data class called Person with two properties: firstName and lastName.
You want to be able to access the person's full name without modifying the Person data class or inheriting from it. You can do this by creating an extension property with a custom get() function:
Just like with extension functions, the Kotlin standard library uses extension properties widely. For example, see the lastIndex property for a CharSequence.
Delegated properties
You already learned about delegation in the Classes and interfaces chapter. You can also use delegation with properties to delegate their property accessors to another object. This is useful when you have more complex requirements for storing properties that a simple backing field can't handle, such as storing values in a database table, browser session, or map. Using delegated properties also reduces boilerplate code because the logic for getting and setting your properties is contained only in the object that you delegate to.
The syntax is similar to using delegation with classes but operates on a different level. Declare your property, followed by the by keyword and the object you want to delegate to. For example:
Here, the delegated property displayName refers to the Delegate object for its property accessors.
Every object you delegate to must have a getValue() operator function, which Kotlin uses to retrieve the value of the delegated property. If the property is mutable, it must also have a setValue() operator function for Kotlin to set its value.
By default, the getValue() and setValue() functions have the following construction:
In these functions:
The
operatorkeyword marks these functions as operator functions, enabling them to overload theget()andset()functions.The
thisRefparameter refers to the object containing the delegated property. By default, the type is set toAny?, but you may need to declare a more specific type.The
propertyparameter refers to the property whose value is accessed or changed. You can use this parameter to access information like the property's name or type. By default, the type is set toKProperty<*>but you can also useAny?. You don't need to worry about changing this in your code.
The getValue() function has a return type of String by default, but you can adjust this if you want.
The setValue() function has an additional parameter value, which is used to hold the new value that's assigned to the property.
So, how does this look in practice? Suppose you want to have a computed property, like a user's display name, that is calculated only once because the operation is expensive and your application is performance-sensitive. You can use a delegated property to cache the display name so that it is only computed once but can be accessed anytime without performance impact.
First, you need to create the object to delegate to. In this case, the object will be an instance of the CachedStringDelegate class:
The cachedValue property contains the cached value. Within the CachedStringDelegate class, add the behavior that you want from the get() function of the delegated property to the getValue() operator function body:
The getValue() function checks whether the cachedValue property is null. If it is, the function assigns the "Default value" and prints a string for logging purposes. If the cachedValue property has already been computed, the property isn't null. In this case, another string is printed for logging purposes. Finally, the function uses the Elvis operator to return the cached value or "Unknown" if the value is null.
Now you can delegate the property that you want to cache (val displayName) to an instance of the CachedStringDelegate class:
This example:
Creates a
Userclass that has two properties in the header,firstName, andlastName, and one property in the class body,displayName.Delegates the
displayNameproperty to an instance of theCachedStringDelegateclass.Creates an instance of the
Userclass calleduser.Prints the result of accessing the
displayNameproperty on theuserinstance.
Note that in the getValue() function, the type for the thisRef parameter is narrowed from Any? type to the object type: User. This is so that the compiler can access the firstName and lastName properties of the User class.
Standard delegates
The Kotlin standard library provides some useful delegates for you so you don't have to always create yours from scratch. If you use one of these delegates, you don't need to define getValue() and setValue() functions because the standard library automatically provides them.
Lazy properties
To initialize a property only when it's first accessed, use a lazy property. The standard library provides the Lazy interface for delegation.
To create an instance of the Lazy interface, use the lazy() function by providing it with a lambda expression to execute when the get() function is called for the first time. Any further calls of the get() function return the same result that was provided on the first call. Lazy properties use the trailing lambda syntax to pass the lambda expression.
For example:
In this example:
There is a
Databaseclass withconnect()andquery()member functions.The
connect()function prints a string to the console, and thequery()function accepts an SQL query and returns a list.There is a
databaseConnectionproperty that is a lazy property.The lambda expression provided to the
lazy()function:Creates an instance of the
Databaseclass.Calls the
connect()member function on this instance (db).Returns the instance.
There is a
fetchData()function that:Creates an SQL query by calling the
query()function on thedatabaseConnectionproperty.Assigns the SQL query to the
datavariable.Prints the
datavariable to the console.
The
main()function calls thefetchData()function. The first time it is called, the lazy property is initialized. The second time, the same result is returned as the first call.
Lazy properties are useful not only when initialization is resource-intensive but also when a property might not be used in your code. Additionally, lazy properties are thread-safe by default, which is particularly beneficial if you are working in a concurrent environment.
For more information, see Lazy properties.
Observable properties
To monitor whether the value of a property changes, use an observable property. An observable property is useful when you want to detect a change in the property value and use this knowledge to trigger a reaction. The standard library provides the Delegates object for delegation.
To create an observable property, you must first import kotlin.properties.Delegates.observable. Then, use the observable() function and provide it with a lambda expression to execute whenever the property changes. Just like with lazy properties, observable properties use the trailing lambda syntax to pass the lambda expression.
For example:
In this example:
There is a
Thermostatclass that contains an observable property:temperature.The
observable()function accepts20.0as a parameter and uses it to initialize the property.The lambda expression provided to the
observable()function:Has three parameters:
_, which refers to the property itself.old, which is the old value of the property.new, which is the new value of the property.
Checks if the
newparameter is greater than25and, depending on the result, prints a string to console.
The
main()function:Creates an instance of the
Thermostatclass calledthermostat.Updates the value of the
temperatureproperty of the instance to22.5, which triggers a print statement with a temperature update.Updates the value of the
temperatureproperty of the instance to27.0, which triggers a print statement with a warning.
Observable properties are useful not only for logging and debugging purposes. You can also use them for use cases like updating a UI or to perform additional checks, like verifying the validity of data.
For more information, see Observable properties.
Practice
Exercise 1
You manage an inventory system at a bookstore. The inventory is stored in a list where each item represents the quantity of a specific book. For example, listOf(3, 0, 7, 12) means the store has 3 copies of the first book, 0 of the second, 7 of the third, and 12 of the fourth.
Write a function called findOutOfStockBooks() that returns a list of indices for all the books that are out of stock.
- Hint 1
Use the
indicesextension property from the standard library.
- Hint 2
You can use the
buildList()function to create and manage a list instead of manually creating and returning a mutable list. ThebuildList()function uses a lambda with a receiver, which you learned about in earlier chapters.
Exercise 2
You have a travel app that needs to display distances in both kilometers and miles. Create an extension property for the Double type called asMiles to convert a distance in kilometers to miles:
- Hint
Remember that extension properties need a custom
get()function.
Exercise 3
You have a system health checker that can determine the state of a cloud system. However, the two functions it can run to perform a health check are performance intensive. Use lazy properties to initialize the checks so that the expensive functions are only run when needed:
Exercise 4
You're building a simple budget tracker app. The app needs to observe changes to the user's remaining budget and notify them whenever it goes below a certain threshold. You have a Budget class that is initialized with a totalBudget property that contains the initial budget amount. Within the class, create an observable property called remainingBudget that prints:
A warning when the value is lower than 20% of the initial budget.
An encouraging message when the budget is increased from the previous value.