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:
val userProfile = loadUserProfile() // called immediatelyYou can do this:
val userProfile by lazy { loadUserProfile() } // called only when accessedThat 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:
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:
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:
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.
