Lazy Initialization in Kotlin Explained: Boost App Performance with Minimal Code

Table of Contents

Ever heard the phrase “don’t fix what isn’t broken”? In coding, a similar mindset applies: don’t load what you don’t need. This is where Lazy Initialization in Kotlin comes in — a slick way to optimize performance, cut unnecessary processing, and keep your codebase clean.

In this post, we’ll break down what lazy initialization is, how it works in Kotlin, and why it can be a game-changer for your Android apps or any Kotlin-based project.

What Is Lazy Initialization?

Lazy initialization is a technique where you delay the creation of an object or the execution of code until it’s actually needed.

Instead of doing this:

Kotlin
val userProfile = loadUserProfile() // called immediately

You can do this:

Kotlin
val userProfile by lazy { loadUserProfile() } // called only when accessed

That one small change tells Kotlin: “Hey..!, don’t run this until someone actually tries to use userProfile.”

Why Use Lazy Initialization in Kotlin?

Kotlin makes lazy initialization incredibly simple and safe. Here are a few reasons to use it:

  • Improved performance: Avoid heavy operations at startup.
  • Memory efficiency: Delay creating large objects until necessary.
  • Cleaner code: Encapsulate logic without creating unnecessary setup.
  • Thread safety: Kotlin provides built-in thread-safe lazy options.

Lazy initialization is especially handy in Android apps where performance at launch is critical.

Real-World Example: Android ViewModel

Let’s say you’re using a ViewModel in your Fragment:

Kotlin
private val viewModel: MyViewModel by lazy {
    ViewModelProvider(this).get(MyViewModel::class.java)
}

Now the ViewModel only gets initialized when you first access viewModel, which can save resources if your fragment has optional UI states or features.

How Lazy Works Under the Hood

When you use by lazy { ... }, Kotlin creates a delegate object that handles initialization. The first time the variable is accessed, the lambda runs and the result is stored. Every future access returns that cached value.

This means:

  • Initialization happens once.
  • The value is memoized (cached).
  • It’s seamless and efficient.

Thread-Safety Options

Kotlin’s lazy has three modes:

Kotlin
lazy(LazyThreadSafetyMode.SYNCHRONIZED) // default
lazy(LazyThreadSafetyMode.PUBLICATION)
lazy(LazyThreadSafetyMode.NONE)
  • SYNCHRONIZED: Safe for multithreaded access. Overhead of synchronization.
  • PUBLICATION: May run initializer multiple times on concurrent access, but only one result is stored.
  • NONE: No thread safety. Fastest, but use only in single-threaded contexts.

Custom Lazy Initialization

Want full control? You can create your own lazy-like delegate:

Kotlin
class CustomLazy<T>(val initializer: () -> T) {
    // 1. Private backing field to hold the actual value
    private var _value: T? = null

    // 2. Public property to access the value, with a custom getter
    val value: T
        get() {
            // 3. Check if the value has been initialized yet
            if (_value == null) {
                // 4. If not, execute the initializer lambda
                _value = initializer()
            }
            // 5. Return the (now initialized) value
            return _value!! // !! asserts that _value is not null
        }
}

val config = CustomLazy { loadConfig() }.value

/////////////////////////////////////////////////////////////////////////////////

//////////////////// Working Code////////////////////////////////////
 
// 1. Define your CustomLazy class
class CustomLazy<T>(val initializer: () -> T) {
    private var _value: T? = null

    val value: T
        get() {
            if (_value == null) {
                println("--- Calling initializer (loadConfig()) for the first time... ---")
                _value = initializer()
                println("--- Initializer finished. ---")
            } else {
                println("--- Value already initialized, returning cached value. ---")
            }
            return _value!!
        }
}

// 2. A sample function that simulates loading configuration
//    (e.g., from a file, network, or complex calculation)
fun loadConfig(): String {
    println(">>> Executing actual loadConfig() function... (This is an expensive operation)")
    // Simulate some delay or heavy computation
    Thread.sleep(1000) // Sleep for 1 second
    return "Application Configuration Data Loaded!"
}

// 3. Main function to demonstrate the usage
fun main() {
    println("Application starting...")

    // This line creates the CustomLazy object, but loadConfig() is NOT called yet.
    // The lambda { loadConfig() } is merely stored.
    val lazyConfigInstance = CustomLazy { loadConfig() }

    println("\nCustomLazy instance created, but config is not loaded yet.")
    println("You can do other things here before accessing config...\n")
    Thread.sleep(500) // Simulate some work

    println("Now, let's access the config value for the first time.")
    // This is where .value is accessed, triggering loadConfig()
    val config1 = lazyConfigInstance.value
    println("Config (first access): \"$config1\"")

    println("\n------------------------------------------------------")
    println("Accessing config value again (should be instant and not re-run loadConfig())...")
    // This access will use the cached value; loadConfig() will NOT be called again.
    val config2 = lazyConfigInstance.value
    println("Config (second access): \"$config2\"")
    println("------------------------------------------------------\n")

    // Another example: If you create a new CustomLazy instance,
    // loadConfig() will run again when its value is first accessed.
    println("Creating another CustomLazy instance and accessing it immediately...")
    val configImmediatelyLoaded = CustomLazy { loadConfig() }.value
    println("Config (immediately loaded): \"$configImmediatelyLoaded\"")

    println("\nApplication finished.")
}

This is just for learning purposes — Kotlin’s built-in lazy does the job better in most cases.

Pitfalls to Watch Out For

  • Heavy lambdas: If the initializer does too much, you’re just delaying pain.
  • Non-idempotent initializers: The initializer should always produce the same result or be side-effect free.
  • Overuse: Don’t lazy-initialize everything. Use it where it adds real benefit.

Conclusion

Lazy Initialization in Kotlin is a powerful yet simple tool. It shines when you want to keep your app responsive and your code clean. Whether you’re building Android apps, desktop tools, or backend services, Kotlin’s by lazy is an elegant way to write smarter code.

Try it out in your project. Start small. Refactor a few variables. You’ll likely see performance gains with very little effort. And that’s the beauty of Kotlin: it lets you do more with less.

Happy Lazy Initialization..!

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!