article banner

Lazy property delegate

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.

The most popular property delegate is lazy. It implements the lazy property pattern, so it postpones read-only value initialization until this value is needed for the first time. This is what an example usage of lazy looks like, and this is what we would need to implement if we didn’t have this delegate1:

val userRepo by lazy { UserRepository() } // Alternative code not using lazy private var _userRepo: UserRepository? = null private val userRepoLock = Any() val userRepo: UserRepository get() { synchronized(userRepoLock) { if (_userRepo == null) { _userRepo = UserRepository() } return _userRepo!! } }

The lazy delegate is often used as a performance optimization for objects that are expensive to calculate. Let's start with an abstract example. Consider class A, which composes classes B, C, and D, each of which is heavy to initialize. This makes the instance of A really heavy to initialize because it requires the initialization of multiple heavy objects.

class A { val b = B() val c = C() val d = D() // ... }

We can make the initialization of A lighter by making b, c, and d lazy properties. Thanks to that, initialization of their values is postponed until their first use; if these properties are never used, the associated class instances will never be initialized. Such an operation improves class creation time, which benefits application startup time and test execution time.

class A { val b by lazy { B() } val c by lazy { C() } val d by lazy { D() } // ... }

To make this example more practical, A might be FlashcardsParser, which is used to parse files defined in a language we have invented; B, C, and D might be some complex regex parsers which are heavy to initialize.

class OurLanguageParser { val cardRegex by lazy { Regex("...") } val questionRegex by lazy { Regex("...") } val answerRegex by lazy { Regex("...") } // ... }

Regex is a really useful notation when we need to process text. However, regex definitions can be very complex, and their parsing is a heavy operation. This reminds me of another example I have famously shown in the Effective Kotlin book. Consider a function in which we use regex to determine whether a string contains a valid IP address:

fun String.isValidIpAddress(): Boolean { return this.matches( ("\\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\z").toRegex() ) } // Usage print("5.173.80.254".isValidIpAddress()) // true

The problem with this function is that the Regex object needs to be created every time we use it. This is a serious disadvantage since regex pattern compilation is complex, therefore this function is unsuitable for repeated use in performance-constrained parts of our code. However, we can improve it by lifting the regex up to the top level:

private val IS_VALID_IP_REGEX = ("\\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\z").toRegex() fun String.isValidIpAddress(): Boolean = matches(IS_VALID_IP_REGEX)

The problem now is that this regex is initialized whenever we initialize this file for the first time; so, if this file contains more elements (like other functions or properties), this regex might slow down the usage of these elements for no good reason. This is why it is better to make the IS_VALID_IP_REGEX property lazy.

private val IS_VALID_IP_REGEX by lazy { ("\\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}" + "(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\z").toRegex() } fun String.isValidIpAddress(): Boolean = matches(IS_VALID_IP_REGEX)

This technique4 can be used in a variety of situations. Consider a data class that stores a computed property3 that is not trivial to calculate. For example, consider the User class with the fullDisplay property, which is calculated from other properties but includes some significant logic as it depends on multiple properties and project configurations. If we define fullDisplay as a regular property, it will be calculated whenever a new instance of User is created, which might be an unnecessary cost.

data class User( val name: String, val surname: String, val pronouns: Pronouns, val gender: Gender, // ... ) { val fullDisplay: String = produceFullDisplay() fun produceFullDisplay(): String { println("Calculating...") // ... return "XYZ" } } fun test() { val user = User(...) // Calculating... val copy = user.copy() // Calculating... println(copy.fullDisplay) // XYZ println(copy.fullDisplay) // XYZ }

If we define the fullDisplay property using a getter, it will be re-calculated whenever it is used, which also might be an unnecessary cost.

data class User( val name: String, val surname: String, val pronouns: Pronouns, val gender: Gender, // ... ) { val fullDisplay: String get() = produceFullDisplay() fun produceFullDisplay(): String { println("Calculating...") // ... return "XYZ" } } fun test() { val user = User(...) val copy = user.copy() println(copy.fullDisplay) // Calculating... XYZ println(copy.fullDisplay) // Calculating... XYZ }

Defining the fullDisplay property as lazy is a sweet spot for properties that:

  • are read-only (lazy can only be used for val),
  • are non-trivial to calculate (otherwise, using lazy is not worth the effort),
  • might not be used for all instances (otherwise, use a regular property),
  • might be used more than once by one instance (otherwise, define a property with a getter).
data class User( val name: String, val surname: String, val pronouns: Pronouns, val gender: Gender, // ... ) { val fullDisplay: String by lazy { produceFullDisplay() } fun produceFullDisplay() { println("Calculating...") // ... } } fun test() { val user = User(...) val copy = user.copy() println(copy.fullDisplay) // Calculating... XYZ println(copy.fullDisplay) // XYZ }

When we consider using a lazy delegate as a performance optimization, we should also consider which thread safety mode we want it to use. It can be specified in an additional mode function argument, which accepts the enum LazyThreadSafetyMode. The following options are available:

  • SYNCHRONIZED is the default and safest option and uses locks to ensure that only a single thread can initialize this delegate instance. This option is also the slowest because synchronization mechanisms introduce some performance costs.
  • PUBLICATION means that the initializer function can be called several times on concurrent access to an uninitialized delegate instance value, but only the first returned value will be used as the value of this delegate instance. If a delegate is used only by a single thread, this option will be slightly faster than SYNCHRONIZED; however, if a delegate is used by multiple threads, we need to cover the possible cost of recalculation of the same value before the value is initialized.
  • NONE is the fastest option and uses no locks to synchronize access to the delegate instance value; so, if the instance is accessed from multiple threads, its behavior is undefined. This mode should not be used unless this instance is guaranteed to never be initialized from more than one thread.
val v1 by lazy { calculate() } val v2 by lazy(LazyThreadSafetyMode.PUBLICATION){ calculate() } val v3 by lazy(LazyThreadSafetyMode.NONE) { calculate() }

In all these examples, we used lazy as a performance optimization, but there are other reasons to use it. In this context, I’d like to share a story from my early days of using Kotlin on Android. It was around 2015. Kotlin was still before the stable release, and the Kotlin community was inventing ways of using its features to help us with everyday tasks. You see, in Android we have a concept of Activity, which is like a window that defines its view, traditionally using XML files. Then, the class representing an Activity often modifies this view by programmatically changing text or some of its properties. To change a particular view element, we need to reference it, but we cannot reference it before the view is set with the setContentView function. Back then, this is why it was standard practice to define references to view elements as lateinit properties and associate appropriate view elements with them immediately after setting up the content view.

class MainActivity : Activity() { lateinit var questionLabelView: TextView lateinit var answerLabelView: EditText lateinit var confirmButtonView: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) questionLabelView = findViewById(R.id.main_question_label) answerLabelView = findViewById(R.id.main_answer_label) confirmButtonView = findViewById(R.id.main_button_confirm) } }

This pattern was used in nearly all Android applications (and I can still see it nowadays in some projects), even though it is far from perfect. We need to define multiple lateinit properties, which are all read-write even though they are initialized only once. Property definition and assignment are separated. If a property is not used, it will not be marked by the IDE because an assignment is considered a usage. Overall, there is a lot of space for improvement. The solution turned out to be extremely simple: we can define property initialization next to its definition if we make it lazy.

class MainActivity : Activity() { val questionLabelView: TextView by lazy { findViewById(R.id.main_question_label) } val answerLabelView: TextView by lazy { findViewById(R.id.main_answer_label) } val confirmButtonView: Button by lazy { findViewById(R.id.main_button_confirm) } override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) } }

Thanks to the lazy delegate, finding the view by id will be postponed until the property is used for the first time, which we can assume will happen after the content view is set. We made such an assumption in the previous solution, so why not make it in this one too? We also have some important improvements: properties that keep view references are read-only, initialization and definition are kept together, and unused properties will be marked. We also have performance benefits: a view reference that is never used will never be associated with the view element, which is good because the findViewById execution can be expensive.

What is more, we can push this pattern further and extract a function that will both define a lazy delegate and find a view by id. In a project I co-created, we named this delegate bindView, and it helped us make our view references really clear, consistent, and readable.

class MainActivity : Activity() { var questionLabelView: TextView by bindView(R.id.main_question_label) var answerLabelView: TextView by bindView(R.id.main_answer_label) var confirmButtonView: Button by bindView(R.id.main_button_confirm) override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.main_activity) } } // ActivityExt.kt fun <T : View> Activity.bindView(viewId: Int) = lazy { this.findViewById<T>(viewId) }

We started using this pattern to bind other references as well, like strings or colors. But my favorite part was binding Activity arguments, which I still use today in some of my projects because it’s very convenient to have all activity properties defined together with their keys at the top of this activity definition.

class SettingsActivity : Activity() { private val titleString by bindString(R.string.title) private val blueColor by bindColor(R.color.blue) private val doctor by extra<Doctor>(DOCTOR_KEY) private val title by extraString(TITLE_KEY) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.settings_activity) } } // ActivityExt.kt fun <T> Activity.bindString(@IdRes id: Int): Lazy<T> = lazy { this.getString(id) } fun <T> Activity.bindColor(@IdRes id: Int): Lazy<T> = lazy { this.getColour(id) } fun <T : Parcelable> Activity.extra(key: String) = lazy { this.intent.extras.getParcelable(key) } fun Activity.extraString(key: String) = lazy { this.intent.extras.getString(key) }

In the backend application, you can also find lazy delegates that are used to initialize properties that should be initialized when they are needed for the first time, but not during project setup. A simple example is a connection to a local database.

As you can see, lazy is a powerful delegate that is mainly used for performance optimization but can also be used to simplify our code. I would like to finish this section with a small puzzle. Consider the following code2:

class Lazy { var x = 0 val y by lazy { 1 / x } fun hello() { try { print(y) } catch (e: Exception) { x = 1 print(y) } } } fun main(args: Array<String>) { Lazy().hello() }

What will be printed? Try to answer this question before reading any further.

To understand the answer, we need to consider three things:

  • Delegate exceptions are propagated out of accessors.
  • A lazy delegate first tries to return a previously calculated value; if no value is already stored, it uses a lambda expression to calculate it. If the calculation process is disturbed with an exception, no value is stored; when we ask for the lazy value the next time, processing starts again.
  • Kotlin's lambda expressions automatically capture variable references; so, when we use x inside a lambda expression, every time we use its reference inside a lambda expression we will receive the current value of x; when we call the lambda expression for the second time, x is 1, so the result should be 1.

So, the answer to this puzzle is "1".

In the next part of this series, I will show you the amazing observable and vetoable property delegates. So, stay tuned!

1:

The actual lazy implementation is more complicated as it has better synchronization, which secures it for concurrent use.

2:

I first heard about this puzzle from Anton Keks' presentation and it can be found in his repository, so I assume he is the author.

3:

By computed property, I mean a property whose value is determined by other properties and therefore can always be recalculated.

4:

Also known as memoization.