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
}
}
}
Example solution in playground
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
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
}
class SuspendLazyTest {
@Test
fun should_produce_value() = runTest {
val lazyValue = suspendLazy { delay(1000); 123 }
assertEquals(123, lazyValue())
assertEquals(1000, currentTime)
}
@Test
fun should_not_recalculate_value() = runTest {
var next = 1
val lazyValue = suspendLazy { delay(1000); next++ }
assertEquals(1, lazyValue())
assertEquals(1, lazyValue())
assertEquals(1, lazyValue())
assertEquals(1, lazyValue())
assertEquals(1000, currentTime)
}
@Test
fun should_not_calculate_value_multiple_times_when_multiple_coroutines_access_it() = runBlocking {
var calculatedTimes = 0
val lazyValue = suspendLazy { delay(1000); calculatedTimes++ }
coroutineScope {
repeat(10_000) {
launch {
lazyValue()
}
}
}
assertEquals(1, calculatedTimes)
}
@Test
fun should_try_again_when_failure_during_value_initialization() = runTest {
var next = 0
val lazyValue = suspendLazy {
val v = next++
if (v < 2) throw Error()
v
}
assertTrue(runCatching { lazyValue() }.isFailure)
assertTrue(runCatching { lazyValue() }.isFailure)
assertEquals(2, lazyValue())
assertEquals(2, lazyValue())
assertEquals(2, lazyValue())
}
@Test
fun should_use_context_of_the_first_caller() = runTest {
var ctx: CoroutineContext? = null
val lazyValue = suspendLazy {
ctx = currentCoroutineContext()
123
}
val name1 = CoroutineName("ABC")
withContext(name1) {
lazyValue()
}
assertEquals(name1, ctx?.get(CoroutineName))
val name2 = CoroutineName("DEF")
withContext(name2) {
lazyValue()
}
assertEquals(name1, ctx?.get(CoroutineName))
}
@Test
fun should_set_is_initialized() = runTest {
val lazyValue = suspendLazy { delay(1000); 123 }
assertEquals(false, lazyValue.isInitialized)
launch { lazyValue() }
assertEquals(false, lazyValue.isInitialized)
advanceTimeBy(1000)
assertEquals(false, lazyValue.isInitialized)
runCurrent()
assertEquals(true, lazyValue.isInitialized)
}
}