
🤔 Problem Statement
In October 2018, a GitHub user proposed introducing a suspending version of Kotlin’s lazy { ... }
function to handle expensive initializations without blocking threads. While lazy
effectively defers initialization until needed, it can still block execution, making it less suitable for coroutine-based, non-blocking applications. To solve this, contributors suggested using async(start = LAZY)
, allowing initialization to be deferred and executed asynchronously on first access. Several custom coroutine-based implementations emerged to bridge this gap, but despite strong interest, the feature was never integrated into the standard Kotlin library.
As of now, the Kotlin standard library does not include a built-in suspending version of the lazy function. The discussion on GitHub Issue #706 concluded without integrating this feature into the library. In the meantime, developers have explored alternative approaches, such as Mr Roman Elizarov, from his gist
📣 📣 📣 Everyone finding this gist via Google! Modern
kotlinx.coroutines
has out-of-the-box support forasyncLazy
with the following expression:val myLazyValue = async(start = CoroutineStart.LAZY) { ... }
. UsemyLazyValue.await()
when you need it.
as well as lazily-started-async. However, it’s important to note that the use of CoroutineStart.LAZY
has been debated within the community. A recent discussion in GitHub Issue #4147 considered discouraging its use due to potential complexities and difficulties in code readability.
Given these considerations, if you require suspending lazy initialization, you might opt for a custom implementation tailored to your specific use case. So, how can we implement true non-blocking lazy initialization in coroutines?
Let’s explore some practical solutions. 🚀
- 🎯 Introduction
- 🔥 Implementation
- 🏆 Conclusion
- 🌐 References
🎯 Introduction
Lazy initialization is a powerful pattern that delays object creation until it’s actually needed, improving performance and resource management. But what if you need to initialize a value asynchronously inside Kotlin coroutines? That’s where LazySuspend
comes in! 🌟
🛠 Why Do We Need LazySuspend
?
Kotlin provides lazy {}
for synchronous lazy initialization, but it does not support suspending functions. Imagine you need to load data from a database or fetch an API response asynchronously. 🤯 Consider this example:
val storageProvider by lazy { initializeStorageProvider() // Cannot be a suspend function 😢 } suspend fun initializeStorageProvider(){ // ... long-running task }
This won’t work if initializeStorageProvider
is a suspend
function! Instead, we need a coroutine-friendly lazy initialization mechanism. 💡
🔥 Implementation
1️⃣ Approach 1
import kotlinx.coroutines.* import kotlin.coroutines.* class LazySuspend<T>(private val initializer: suspend () -> T) { @Volatile private var cachedValue: T? = null private val mutex = Mutex() suspend fun getValue(): T { if (cachedValue != null) return cachedValue!! return mutex.withLock { if (cachedValue == null) { cachedValue = initializer() } cachedValue!! } } }
- ✅ Uses a suspending function for initialization.
- ✅ Uses a mutex (
withLock) to ensure thread safety (prevents race conditions in multithreading).
- ✅ Stores the computed value after the first call, so subsequent calls return instantly.
suspend fun main() { val lazyValue = LazySuspend { println("Initializing...") delay(1000) // Simulate long computation "Hello, Coroutine Lazy!" } println("Before accessing value...") println("Value: ${lazyValue.getValue()}") // Triggers initialization println("Value again: ${lazyValue.getValue()}") // Uses cached value } // output Before accessing value... Initializing... Value: Hello, Coroutine Lazy! Value again: Hello, Coroutine Lazy!
2️⃣ Approach 2: Deferred
class LazySuspendDeferred<T>(scope: CoroutineScope, initializer: suspend () -> T) { private val deferred = scope.async(start = CoroutineStart.LAZY) { initializer() } suspend fun getValue(): T = deferred.await() }
3️⃣ Approach 3: SuspendLazy from kt.academy
This function allows deferred execution of a block of code that is initialized only once in a coroutine, similar to lazy initialization. It ensures thread safety by using a Mutex
and provides mechanisms to handle initialization failures and context propagation, for further details, visit the original article.
import kotlinx.coroutines.* | |
import kotlinx.coroutines.sync.Mutex | |
import kotlinx.coroutines.sync.withLock | |
import kotlinx.coroutines.test.advanceTimeBy | |
import kotlinx.coroutines.test.currentTime | |
import kotlinx.coroutines.test.runCurrent | |
import kotlinx.coroutines.test.runTest | |
import kotlin.coroutines.CoroutineContext | |
import org.junit.Test | |
import kotlin.test.assertEquals | |
import kotlin.test.assertTrue | |
/** | |
* https://kt.academy/article/s_suspended_lazy | |
*/ | |
fun <T> suspendLazy(initializer: suspend () -> T): SuspendLazy<T>{ | |
var innerInitializer: (suspend () -> T)? = initializer | |
val mutex = Mutex() | |
var holder: Any? = Any() | |
return object : SuspendLazy<T> { | |
override val isInitialized: Boolean | |
get() = innerInitializer == null | |
override fun valueOrNull(): T? = | |
if (isInitialized) holder as T else null | |
@Suppress("UNCHECKED_CAST") | |
override suspend fun invoke(): T = | |
if (isInitialized) holder as T | |
else mutex.withLock { | |
innerInitializer?.let { | |
holder = it() | |
innerInitializer = null | |
} | |
holder as T | |
} | |
} | |
} | |
interface SuspendLazy<T> : suspend () -> T { | |
val isInitialized: Boolean | |
fun valueOrNull(): T? | |
override suspend operator fun invoke(): T | |
} |
import kotlinx.coroutines.* | |
import kotlinx.coroutines.sync.Mutex | |
import kotlinx.coroutines.sync.withLock | |
import kotlinx.coroutines.test.advanceTimeBy | |
import kotlinx.coroutines.test.currentTime | |
import kotlinx.coroutines.test.runCurrent | |
import kotlinx.coroutines.test.runTest | |
import kotlin.coroutines.CoroutineContext | |
import org.junit.Test | |
import kotlin.test.assertEquals | |
import kotlin.test.assertTrue | |
/** | |
* https://kt.academy/article/s_suspended_lazy | |
*/ | |
fun <T> suspendLazy(initializer: suspend () -> T): SuspendLazy<T>{ | |
var innerInitializer: (suspend () -> T)? = initializer | |
val mutex = Mutex() | |
var holder: Any? = Any() | |
return object : SuspendLazy<T> { | |
override val isInitialized: Boolean | |
get() = innerInitializer == null | |
override fun valueOrNull(): T? = | |
if (isInitialized) holder as T else null | |
@Suppress("UNCHECKED_CAST") | |
override suspend fun invoke(): T = | |
if (isInitialized) holder as T | |
else mutex.withLock { | |
innerInitializer?.let { | |
holder = it() | |
innerInitializer = null | |
} | |
holder as T | |
} | |
} | |
} | |
interface SuspendLazy<T> : suspend () -> T { | |
val isInitialized: Boolean | |
fun valueOrNull(): T? | |
override suspend operator fun invoke(): T | |
} |
4️⃣ Approach 4: LazySuspend from ME ✌️😊
Why I choose LazySuspend instead of SuspendLazy?
Both LazySuspend and SuspendLazy are reasonable names, but the better choice depends on readability, consistency, and convention.
- Matches the existing
lazy { ... }
function in Kotlin. - Emphasizes “lazy” behavior first, making it clear this is an alternative to
lazy { ... }
. - Easier to recognize for Kotlin developers already familiar with
lazy
.
The approach 3 may have some ✨ potential improvements, so that’s why I come up with LazySuspend
✅ Avoid Unsafe Casts: The code currently casts holder
to T
, which might cause issues if holder
was never properly assigned. Instead, you can use a sealed class or an AtomicReference
.
🔐 Ensuring Thread-Safety with Mutex we ensure that only one coroutine initializes the value at a time, preventing race conditions. 🏎💨
⚠️ Handling Exceptions Gracefully: If the initializer
fails, holder
remains Any?
, causing an unsafe cast, leading to a ClassCastException.
package com.nphausg.loom.coroutine | |
import kotlinx.coroutines.sync.Mutex | |
import kotlinx.coroutines.sync.withLock | |
import java.util.concurrent.atomic.AtomicReference | |
/** | |
* Sealed class representing the state of a lazily initialized value. | |
* It can either be uninitialized, initialized with a value, or failed due to an exception. | |
* | |
* @param T The type of the value being lazily initialized. | |
*/ | |
sealed class LazyState<out T> { | |
/** | |
* Represents an uninitialized state. | |
*/ | |
data object Uninitialized : LazyState<Nothing>() | |
/** | |
* Represents an initialized state holding a value of type [T]. | |
* | |
* @param value The initialized value of type [T]. | |
*/ | |
data class Initialized<T>(val value: T) : LazyState<T>() | |
/** | |
* Represents a failed state containing an exception. | |
* | |
* @param exception The exception that occurred during initialization. | |
*/ | |
data class Failed(val exception: Throwable) : LazyState<Nothing>() | |
} | |
/** | |
* A functional interface representing a suspending function that can return a value of type [T]. | |
* This interface also provides properties and functions to check the initialization state and | |
* retrieve the value if it's available. | |
*/ | |
interface LazySuspend<T> : suspend () -> T { | |
/** | |
* A property indicating if the value has been initialized. | |
*/ | |
val isInitialized: Boolean | |
/** | |
* Returns the value if it's initialized, or `null` if it's uninitialized or failed. | |
* | |
* @return The initialized value of type [T] or `null`. | |
*/ | |
fun getOrNull(): T? | |
/** | |
* Suspends the execution and retrieves the lazily initialized value. | |
* If not initialized, it will invoke the initializer function. | |
* | |
* @return The lazily initialized value of type [T]. | |
*/ | |
override suspend operator fun invoke(): T | |
} | |
/** | |
* Creates a [LazySuspend] instance, which lazily initializes a value using the provided [initializer]. | |
* The initialization is done in a thread-safe manner, using double-checked locking. | |
* | |
* If the initialization fails, the state will be marked as failed and the exception will be thrown. | |
* | |
* @param T The type of the lazily initialized value. | |
* @param initializer A suspending function that provides the value to be lazily initialized. | |
* @return A [LazySuspend] instance that encapsulates the lazy initialization logic. | |
* @author <a href="https://github.com/nphausg">nphausg</> | |
*/ | |
fun <T> lazySuspend(initializer: suspend () -> T): LazySuspend<T> { | |
val state = AtomicReference<LazyState<T>>(LazyState.Uninitialized) | |
val mutex = Mutex() | |
return object : LazySuspend<T> { | |
override val isInitialized: Boolean | |
get() = state.get() is LazyState.Initialized | |
override fun getOrNull(): T? = | |
(state.get() as? LazyState.Initialized)?.value | |
override suspend fun invoke(): T { | |
val currentState = state.get() | |
if (currentState is LazyState.Initialized) { | |
return currentState.value | |
} | |
return mutex.withLock { | |
val doubleCheck = state.get() | |
if (doubleCheck is LazyState.Initialized) { | |
return doubleCheck.value | |
} | |
try { | |
val result = initializer() | |
state.set(LazyState.Initialized(result)) | |
result | |
} catch (e: Throwable) { | |
state.set(LazyState.Failed(e)) | |
throw e | |
} | |
} | |
} | |
} | |
} |
https://gist.github.com/nphausg/d370986b1575b7c75085a6132bc123ae
LazyState Sealed Class: This class is used to represent the current state of a value, whether it’s uninitialized, initialized with a value, or failed due to an exception.
LazySuspend Interface: This interface extends a suspending function (
suspend () -> T
) and adds additional properties and methods:isInitialized
: A boolean property to check if the value has been initialized.getOrNull()
: Returns the value if initialized, or null if not.invoke()
: The main suspending function to retrieve the lazily initialized value.lazySuspend Function: This function creates an instance of
LazySuspend
that lazily initializes a value using the provided suspending function (initializer
). It uses atomic references for thread-safety and double-checked locking to ensure that the value is initialized only once.
How can I ensure that the
LazySuspend
initialization runs on a background thread instead of the main thread in Kotlin?
To ensure that the lazySuspend
initialization does not run on the main thread, you can explicitly use a different coroutine dispatcher when invoking the suspending function inside the initializer. You can use Dispatchers.IO
, Dispatchers.Default
, or any custom dispatcher to offload the work to a background thread. Here’s how you can modify your lazySuspend
initialization to run on a background thread:
import kotlinx.coroutines.* val lazyValue = lazySuspend { withContext(Dispatchers.IO) { // Ensure this runs on a background thread println("Initialized on thread: ${Thread.currentThread().name}") longRunningTask() // Simulate some background work } }
Job Offers
🚨 Disclaimers
Approach 4 may have some disadvantages:
- Complexity: The custom implementation adds complexity compared to Kotlin’s built-in
lazy
. - Potential Overhead: Double-checked locking may introduce unnecessary overhead in single-threaded scenarios.
- Limited Use Case: The extra control may not be required for simpler lazy initialization needs.
🏆 Conclusion
The LazySuspend interface includes methods to check if the value is initialized (
isInitialized), retrieve it if available (
getOrNull()), and lazily initialize it when accessed (
invoke()). The
LazySuspend
provides lazy, suspend-aware initialization while ensuring thread safety and error handling. 🚀 Whether you’re fetching API data, caching results, or managing expensive computations, LazySuspend
is a powerful tool in your Kotlin arsenal.
Give it a try in your next project! 🛠️
// Step 1: Grab from Maven central at the coordinates: repositories { google() mavenCentral() maven { url = uri("https://maven.pkg.github.com/nphausg/loomIn") } } // Step 2: Implementation from your module $latestVersion = "0.0.1-alpha" implementation("com.nphausg:loom:$latestVersion")
🌐 References
- Kotlin Coroutines Documentation — Kotlinlang.org
- Mutex in Kotlin Coroutines — Kotlin Coroutines Guide
- Lazy Initialization in Kotlin — JetBrains Blog
- AtomicReference in Java — Java Documentation
This article is previously published on proandroiddev.com.