Rahul Chhodde I'm a software developer with over seven years of experience in different web technologies.

Initializing lazy and lateinit variables in Kotlin

7 min read 2126

Initializing Lazy And Lateinit Variables In Kotlin

Kotlin usually requires us to initialize properties as soon as we define them. Doing this seems odd when we don’t know the ideal initial value, especially in the case of lifecycle-driven Android properties.

Luckily, there is a way to get through this problem. The IntelliJ IDEA editor will warn you if you declare a class property without initializing it and recommend adding a lateinit keyword.

What if an initialized property or object doesn’t actually get used in the program? Well, these unused initializations will be liabilities to the program since object creation is a heavy process. This is another example of where lateinit can come to our rescue.

This article will explain how the lateinit modifier and lazy delegation can take care of unused or unnecessary early initializations. This will make your Kotlin development workflow more efficient altogether.

lateinit in Kotlin

The lateinit keyword stands for “late initialization.” When used with a class property, the lateinit modifier keeps the property from being initialized at the time of its class’ object construction.

Memory is allocated to lateinit variables only when they are initialized later in the program, rather than when they are declared. This is very convenient in terms of flexibility in initialization.

Let’s look at some important features that lateinit has to offer!

Key features

Firstly, memory is not allocated to a lateinit property at the time of declaration. The initialization takes place later when you see fit.

A lateinit property may change more than once throughout the program and is supposed to be mutable. That’s why you should always declare it as a var and not as a val or const.

The lateinit initialization can save you from repetitive null checks that you might need when initializing properties as nullable types. This feature of lateinit properties doesn’t support the nullable type.



Expanding on my last point, lateinit can be used well with non-primitive data types. It doesn’t work with primitive types like long or int. This is because whenever a lateinit property is accessed, Kotlin provides it a null value under the hood to indicate that the property has not been initialized yet.

Primitive types can’t be null, so there is no way to indicate an uninitialized property. In consequence, primitive types throw an exception when used with the lateinit keyword.

Lastly, a lateinit property must be initialized at some point before it is accessed or it will throw an UninitializedPropertyAccessException error, as seen below:

Uninitialized Property Access Exception Error

A lateinit property accessed before initialization leads to this exception.

Kotlin allows you to check if a lateinit property is initialized. This can be handy to deal with the uninitialization exception we just discussed.

lateinit var myLateInitVar: String
...

if(::myLateInitVar.isInitialized) {
  // Do something
}

Examples of the lateinit modifier in use

Let’s see the lateinit modifier in action with a simple example. The code below defines a class and initializes some of its properties with dummy and null values.

class TwoRandomFruits {
  var fruit1: String = "tomato" 
  var fruit2: String? = null


  fun randomizeMyFruits() {
      fruit1 = randomFruits()
      fruit2 = possiblyNullRandomFruits()
  }


  fun randomFruits(): String { ... }
  fun possiblyNullRandomFruits(): String? { ... }
}

fun main() {
    val rf= RandomFruits()
    rf.randomizeMyFruits()


    println(rf.fruit1.capitalize())
    println(rf.fruit2?.capitalize()) // Null-check
}

This isn’t the best way to initialize a variable, but in this case, it still does the job.

As you can see above, if you choose to make the property nullable, you’ll have to null check it whenever you modify or use it. This can be rather tedious and annoying.

Let’s tackle this issue with the lateinit modifier:

class TwoRandomFruits {
  lateinit var fruit1: String // No initial dummy value needed
  lateinit var fruit2: String // Nullable type isn't supported here


  fun randomizeMyFruits() {
      fruit1 = randomFruits()
      fruit2 = when {
          possiblyNullRandomFruits() == null -> "Tomato" // Handling null values
          else -> possiblyNullRandomFruits()!!
      }
  }


  fun randomFruits(): String { ... }
  fun possiblyNullRandomFruits(): String? { ... }
}

fun main() {
    val rf= RandomFruits()
    rf.randomizeMyFruits()


    println(rf.fruit1.capitalize())
    println(rf.fruit2.capitalize())
}

You can see this code in action here.

The lateinit implementation speaks for itself and demonstrates a neat way to deal with variables! Apart from the default behavior of lateinit, the main takeaway here is how easily we can avoid using the nullable type.

Lifecycle-driven properties and lateinit

Data binding is another example of using lateinit to initialize an activity later on. Developers often want to initialize the binding variable earlier to use it as a reference in other methods for accessing different views.


More great articles from LogRocket:


In the MainActivity class below, we declared the binding with the lateinit modifier to achieve the same thing.

package com.test.lateinit

import androidx.appcompat.app.AppCompatActivity
import ...

class MainActivity : AppCompatActivity() {
  lateinit var binding: ActivityMainBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

    ...
  }
  ...
}

The binding for MainActivity can only get initialized once the activity lifecycle function, onCreate(), gets fired. Therefore, declaring the binding with the lateinit modifier makes complete sense here.

When to use lateinit

With regular variable initialization, you have to add a dummy and, most likely, a null value. This will add a lot of null checks whenever they’re accessed.

// Traditional initialization
var name: String? = null
...    
name = getDataFromSomeAPI()
...
// A null-check will be required whenever `name` is accessed.
name?.let { it-> 
  println(it.uppercase())
}

// Lateinit initialization
lateinit var name: String
...
name = getDatafromSomeAPI()
...
println(name.uppercase())

We can use the lateinit modifier to avoid these repeated null checks, particularly when a property is likely to fluctuate frequently.

Things to remember when using lateinit

It’s good to remember to always initialize a lateinit property before accessing it, otherwise, you’ll see a big exception thrown at the time of compilation.

Make sure to also keep the property mutable by using a var declaration. Using val and const won’t make any sense, as they indicate immutable properties with which lateinit will not work.

Finally, avoid using lateinit when the given property’s data type is primitive or the chances of a null value are high. It’s not made for these cases and doesn’t support primitive or nullable types.

Lazy delegation in Kotlin

As the name suggests, lazy in Kotlin initializes a property in a lazy manner. Essentially, it creates a reference but only goes for the initialization when the property is used or called for the first time.

Now, you may be asking how this is different from regular initialization. Well, at the time of a class object construction, all of its public and private properties get initialized within its constructor. There’s some overhead associated with initializing variables in a class; the more variables, the greater the overhead will be.

Let’s understand it with an example:

class X {
  fun doThis() {}
}

class Y {
  val shouldIdoThis: Boolean = SomeAPI.guide()
  val x = X()

  if(shouldIdoThis) {
    x.doThis()
  }
  ...
}

Despite not using it, class Y in the above code still has an object created of class X. Class X will also slow down Y if it’s a heavily built class.

Unnecessary object creation is inefficient and may slow down the current class. It could be that some properties or objects are not required under certain conditions, depending on the program flow.

It could also be that properties or objects rely on other properties or objects for creation. Lazy delegation deals with these two possibilities efficiently.

Key features

A variable with lazy initialization will not be initialized until it’s called or used. This way, the variable is initialized only once and then its value is cached for further use in the program.

Since a property initialized with lazy delegation is supposed to use the same value throughout, it is immutable in nature and is generally used for read-only properties. You must mark it with a val declaration.

It is thread-safe, i.e. computed only once and shared by all threads by default. Once initialized, it remembers or caches the initialized value throughout the program.

In contrast to lateinit, lazy delegation supports a custom setter and getter that allows it to perform intermediate operations while reading and writing the value.

Example of lazy delegation in use

The code below implements simple math to calculate the areas of certain shapes. In the case of a circle, the calculation would require a constant value for pi.

class Area {
  val pi: Float = 3.14f


  fun circle(radius: Int): Float = pi * radius * radius
  fun rectangle(length: Int, breadth: Int = length): Int = length * breadth
  fun triangle(base: Int, height: Int): Float = base * height * .5f
}

fun main() {
  val area = Area()
  val squareSideLength = 51


  println("Area of our rectangle is ${area.rectangle(squareSideLength)}")
}

As you can see above, no calculation of the area of any circle was completed, making our definition of pi useless. The property pi still gets initialized and allocated memory.

Let’s rectify this issue with the lazy delegation:

class Area {
  val pi: Float by lazy {
    3.14f
  } 


  fun circle(...) = ...
  fun rectangle(...) = ...
  fun triangle(...) = ...
}

fun main() {
  val area = Area()
  val squareSideLength = 51
  val circleRadius = 37

  println("Area of our rectangle is ${area.rectangle(squareSideLength)}")
  println("Area of our circle is ${area.circle(circleRadius)}")
}

You can see a demo of the above example here.

The above implementation of lazy delegation makes use of pi only when it is accessed. Once accessed, its value is cached and reserved to use throughout the program. We’ll see it in action with objects in the next examples.

Intermediate actions

Here’s how you can add some intermediate actions while writing values via lazy delegation. The below code lazy initializes a TextView in an Android activity.

Whenever this TextView gets called for the first time within the MainActivity, a debug message with a LazyInit tag will be logged, as shown below in the lambda function of the delegate:

...
class MainActivity : AppCompatActivity() {
  override fun onCreate(...) {
    ...  
    val sampleTextView: TextView by lazy {
      Log.d("LazyInit", "sampleTextView")
      findViewById(R.id.sampleTextView)
    }
  }
  ...
}

Lazy delegation in Android apps

Now let’s move on to the application of lazy delegation in Android apps. The simplest use case can be our previous example of an Android activity that uses and manipulates a view conditionally.

package com.test.lazy

import androidx.appcompat.app.AppCompatActivity
import ...

class MainActivity : AppCompatActivity() {
  lateinit var binding: ActivityMainBinding

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)


    val sharedPrefs by lazy {
      activity?
        .getPreferences(Context.MODE_PRIVATE)
    } 


    val startButton by lazy {
      binding.startButton
    }


    if(sharedPrefs.getBoolean("firstUse", true)) {
      startButton.isVisible = true
      startButton.setOnClickListener {
        // Finish onboarding, move to main screen; something like that
        sharedPrefs.setBoolean("firstUse", false)
      }
    }

  }
}

Above, we initialized the SharedPreferences and a Button with lazy delegation. The logic entails the implementation of an onboarding screen based on a boolean value fetched from shared preferences.

The difference between by lazy and = lazy

The by lazy statement adds an enhancement by the lazy delegate directly to a given property. Its initialization will happen only once upon its first access.

val prop by lazy {
  ...
}

On the other hand, the = lazy statement holds a reference to the delegate object instead, by which you may use the isInitialized() delegation method or access it with the value property.

val prop = lazy {
  ...
}
...

if(prop.isInitialized()) {
  println(prop.value)
}

You can see a quick demo of the above code here.

When to use lazy

Consider using lazy delegates to lighten a class that involves multiple and/or conditional creations of other class objects. If the object creation depends on an internal property of the class, lazy delegation is the way to go.

class Employee {
    ...
    fun showDetails(id: Int): List<Any> {
        val employeeRecords by lazy {
            EmployeeRecords(id) // Object's dependency on an internal property
        }
    }
    ...
}

Things to remember when using lazy

Lazy initialization is a delegation that initializes something only once and only when it’s called. It’s meant to avoid unnecessary object creation.

The delegate object caches the value returned on first access. This cached value is used further in the program when required.

You may take advantage of its custom getter and setter for intermediate actions when reading and writing values. I also prefer using it with immutable types, as I feel it works best with values that stay unchanged throughout the program.

Conclusion

In this article, we discussed Kotlin’s lateinit modifier and lazy delegation. We showed some basic examples demonstrating their uses and also talked about some practical use cases in Android development.

Thank you for taking the time to read this starter through the end! I hope you’ll be able to use this guide to implement these two features in your app development journey.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Rahul Chhodde I'm a software developer with over seven years of experience in different web technologies.

Leave a Reply