Upcoming and OnDemand Webinars View full list

Kotlin: When to Use Lazy or Lateinit

Bolot Kerimbaev

You start the development sprint full of energy, but the ancient curse of Java bogs you down and you realize you are in for a marathon.

“Is it safe?” the massive code base keeps asking you.

“Is it safe?” forcing you to check whether your variables are null.

“Is it safe?” the sadistic voice is relentless.

“It’s so safe you won’t believe it!” you utter, but you are not sure anymore.

Is It Safe? With Kotlin It Is

Java does not protect you from the “billion dollar mistake” – the null pointer is lurking everywhere. Every reference can potentially be null. How can anyone be safe in Java?

Many Android developers find refuge in Kotlin, a modern programming language that eliminates a lot of pain and suffering. Less boilerplate, more expressive code and, yes, it is null safe – if you choose the path of avoiding torture.

Still, Kotlin has to live in the world where Java was king, and on Android the Activity lifecycle further complicates things. Consider, for example, storing a reference to a view in a property. This is a common practice because Android developers try to avoid repeated calls to findViewById.

Ideally, an object’s properties would all be defined at the time it is created, but, because for Activities and Fragments object creation is separate from view loading, the properties intended to store views must start out uninitialized.

This post explores several approaches to handling properties that reference views:

  • Using a nullable type
  • Using lateinit
  • Using a custom getter
  • Using by lazy
  • Two custom property delegates

Nullable Type

The simplest way to reference a view in a property is to use a nullable type.

var showAnswerButton: Button? = null

Since all variables must be initialized, null is assigned to showAnswerButton. Later, in Activity.onCreate (or Fragment.onCreateView), it will be assigned again, this time the value that we actually want it to have.

showAnswerButton = findViewById(R.id.showAnswerButton)

When using a nullable type, the ?. or !! operators have to be used to access the nullable variable. Using ?. avoids a crash by returning null should showAnswerButton be null for some reason.

showAnswerButton?.setOnClickListener { /* */ }

This is equivalent to the following code in Java:

if (showAnswerButton != null) {
    showAnswerButton.setOnClickListener(/* */);
}

The !! operator would cause a crash in the following code if showAnswerButton were null:

showAnswerButton!!.setOnClickListener { /* */ }

Using these operators at least make it obvious that showAnswerButton is nullable. This is not as easy to spot in Java:

showAnswerButton.setOnClickListener(/* */);

Lateinit

There is a better alternative: lateinit.

lateinit var questionTextView: TextView

Using lateinit, the initial value does not need to be assigned. Furthermore, at the use sites the questionTextView is not a nullable type, so ?. and !! are not used. However, we have to be careful to assign our lateinit var a value before we use it. Otherwise, a lateinit property acts as if we performed !!: it will crash the app on a null value.

Custom getter

You could also create a property with a custom getter:

val anotherTextView: TextView
    get() = findViewById(R.id.another_text_view)

This approach has a big drawback, each time the property is accessed, the findViewById method is called. Furthermore, for Fragments, you have to use the !! operator on the view property. Bang-bang – any illusion of null safety is gone.

Lazy

A property defined via by lazy is initialized using the supplied lambda upon first use, unless a value had been previously assigned.

val nameTextView by lazy { view!!.findViewById<TextView>(R.id.nameTextView) }

This approach will cause a crash if nameTextView is accessed before setContentView in an Activity. It is even trickier in Fragments, because this code would cause a crash inside onCreateView even after the view is inflated.
That’s because the view property of the Fragment is not set until after onCreateView completes, and it is referenced in the lazy variable’s initializer. It is possible to use lazy properties in onViewCreated.

Using a lazily initialized property on a retained fragment causes a memory leak, since the property holds a reference to the old view.

Custom Property Delegate

Unlike other languages, lazy in Kotlin is not a language feature, but a property delegate implemented in the standard library. Thus, it is possible to draw on it as inspiration and perform a mind experiment. Would it be possible to resolve the memory leak and the lack of lifecycle-awareness of the by lazy approach?

Android Architecture Components includes support for lifecycle awareness.
The cityTextView property is defined using LifecycleAwareLazy, which takes in a Lifecycle instance and clears out the value when the ON_STOP event occurs. This ensures that the value is initialized again.

val cityTextView by lifecycleAwareLazy(lifecycle) { view!!.findViewById<TextView>(R.id.cityTextView) }

This creates an interesting curiosity: while cityTextView has been defined as a val, by virtue of the property delegate it can still change, as if it were a var. In fact, properties that have a delegate cannot even be declared with a var.

The stateTextView is defined using LifecycleAwareFindView, which takes a Fragment that also happens to implement the LifecycleOwner interface and the view ID.

val stateTextView: TextView by findView(this, R.id.stateTextView)

These two property delegates solve one problem, but not completely.
They still contain a memory leak.

When to Use Lazy or Lateinit

Lazy is a good fit for properties that may or may not be accessed.
If we never access them, we avoid computing their initial value.
They may work for Activities, as long as they are not accessed before setContentView is called. They are not a great fit for referencing views in a Fragment, because the common pattern of configuring views inside onCreateView would cause a crash. They can be used if view configuration is done in onViewCreated.

With Activities or Fragments, it makes more sense to use lateinit for their properties, especially the ones referencing views. While we don’t control the lifecycle, we know when those properties will be properly initialized. The downside is we have to ensure they are initialized in the appropriate lifecycle methods.

References

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project