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:
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.
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.
The post Initializing lazy and <code>lateinit</code> variables in Kotlin appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/madF0K9
Gain $200 in a week
via Read more