Last active
May 6, 2022 15:25
-
-
Save NinoDLC/89f0f7992ed0082f991c916752b9ae6a to your computer and use it in GitHub Desktop.
DataInterpolationRepository
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Kotlin Playground here : https://pl.kotl.in/D6wbZO2k6