Skip to content

Instantly share code, notes, and snippets.

@NinoDLC
Last active May 6, 2022 15:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NinoDLC/89f0f7992ed0082f991c916752b9ae6a to your computer and use it in GitHub Desktop.
Save NinoDLC/89f0f7992ed0082f991c916752b9ae6a to your computer and use it in GitHub Desktop.
DataInterpolationRepository
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicLong
/**
* Interpolates any data [D] matched against a unique [ID] for an [interpolationDuration] (default is 2 seconds), removing the entry from
* the emitted map after this delay.
*/
open class DataInterpolationRepository<ID : Any, D : Any>(
private val globalScope: CoroutineScope,
private val interpolationDuration: Duration = 2.seconds,
) {
private val interpolationId = AtomicLong()
private val map = mutableMapOf<ID, InterpolationStatus<D>>()
private val interpolatedStatusMutableStateFlow = MutableSharedFlow<MutableMap<ID, InterpolationStatus<D>>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
).apply {
tryEmit(map)
}
fun put(id: ID, data: D): Long = interpolationId.getAndIncrement().also { interpolationId ->
update(id, InterpolationStatus.Present(interpolationId = interpolationId, data = data))
}
fun remove(id: ID): Long = interpolationId.getAndIncrement().also { interpolationId ->
update(id, InterpolationStatus.Absent(interpolationId = interpolationId))
}
fun invalidate(interpolationId: Long) {
map.entries.find { it.value.interpolationId == interpolationId }?.let { matchingEntry ->
map.remove(matchingEntry.key)
interpolatedStatusMutableStateFlow.tryEmit(map)
}
}
/**
* @param realDataMapFlow The real data flow you want to merge interpolation data with
*/
fun interpolatedWithRealData(realDataMapFlow: Flow<Map<ID, D>>): Flow<Map<ID, D>> = combine(
realDataMapFlow,
interpolatedStatusMutableStateFlow
) { realDataMap: Map<ID, D>, interpolatedStatusMap: Map<ID, InterpolationStatus<D>> ->
val allIds: Set<ID> = realDataMap.keys + interpolatedStatusMap.keys
buildMap<ID, D>(allIds.size) {
allIds.forEach { id ->
val interpolatedStatus = interpolatedStatusMap[id]
if (interpolatedStatus is InterpolationStatus.Present) {
put(id, interpolatedStatus.data)
} else if (interpolatedStatus == null) {
val realData = realDataMap[id]
if (realData != null) {
put(id, realData)
}
}
}
}
}.distinctUntilChanged()
private fun update(id: ID, data: InterpolationStatus<D>) {
// Global scope use because if the scope is killed during the delay, the value will always be interpolated...
globalScope.launch {
map[id] = data
interpolatedStatusMutableStateFlow.tryEmit(map)
delay(interpolationDuration)
if (map[id]?.interpolationId == data.interpolationId) {
map.remove(id)
interpolatedStatusMutableStateFlow.tryEmit(map)
}
}
}
private sealed class InterpolationStatus<out T> {
abstract val interpolationId: Long
data class Present<out T>(
override val interpolationId: Long,
val data: T,
) : InterpolationStatus<T>()
data class Absent(
override val interpolationId: Long
) : InterpolationStatus<Nothing>()
}
}
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.coroutines.EmptyCoroutineContext
object DataInterpolationRepositoryDemo {
@JvmStatic
fun main(args: Array<String>) = runBlocking {
// Use a GlobalScope or a CoroutineScope not tied with navigation events
val coroutineScope = CoroutineScope(EmptyCoroutineContext)
// 2 seconds interpolation by default, can be changed there
val interpolationRepository = DataInterpolationRepository<Long, DetailEntity>(coroutineScope)
val collectJob = launch {
val start = System.currentTimeMillis()
interpolationRepository.interpolatedWithRealData(
realDataMapFlow = flowOf(
buildMap {
put(1L, getDetailEntity(1))
put(2L, getDetailEntity(2))
put(3L, getDetailEntity(3))
}
)
).collect { map ->
println("${System.currentTimeMillis() - start}ms: $map")
}
}
delay(100) // [1, 2, 3]
interpolationRepository.put(4, getDetailEntity(4))
delay(100) // [1, 2, 3, 4]
val interpolationIdFor5 = interpolationRepository.put(5, getDetailEntity(5))
delay(100) // [1, 2, 3, 4, 5]
interpolationRepository.remove(2)
delay(100) // [1, 3, 4, 5]
interpolationRepository.remove(3)
delay(100) // [1, 4, 5]
interpolationRepository.put(2, getDetailEntity(2))
delay(100) // [1, 2, 4, 5]
interpolationRepository.invalidate(interpolationIdFor5) // If API request fails for example
delay(100) // [1, 2, 4]
delay(1_700) // [1, 2] : put(4) expired
delay(100) // [1, 2, 3] : remove(3) expired
delay(2_100) // no re-emission if collection doesn't change
println("End")
collectJob.cancelAndJoin()
}
private fun getDetailEntity(it: Int) = DetailEntity(
id = it.toString(),
name = "name$it"
)
data class DetailEntity(
val id: String,
val name: String,
)
}
@NinoDLC
Copy link
Author

NinoDLC commented May 6, 2022

Kotlin Playground here : https://pl.kotl.in/D6wbZO2k6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment