feat: add ScheduleCache with multiplatform-settings for offline caching

This commit is contained in:
2026-07-08 17:10:45 +02:00
parent b1661aaa4f
commit 11c0a92421
4 changed files with 200 additions and 0 deletions
+1
View File
@@ -36,6 +36,7 @@ kotlin {
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.multiplatform.settings)
implementation(libs.multiplatform.settings.no.arg)
implementation(libs.lifecycle.viewmodel.compose)
@@ -0,0 +1,76 @@
package ch.parano.myice.cache
import ch.parano.myice.models.Event
import com.russhwolf.settings.Settings
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
@OptIn(ExperimentalTime::class)
class ScheduleCache(private val settings: Settings) {
private val json = Json { ignoreUnknownKeys = true }
private val eventListSerializer = ListSerializer(Event.serializer())
private fun eventsKey(account: String) = "cached_events_$account"
private fun agegroupKey(account: String) = "filters_agegroup_$account"
private fun subgroupKey(account: String) = "filters_subgroup_$account"
private fun typeFilterKey(account: String) = "filters_typefilter_$account"
private fun timestampKey(account: String) = "cached_events_timestamp_$account"
fun saveEvents(account: String, events: List<Event>) {
settings.putString(eventsKey(account), json.encodeToString(eventListSerializer, events))
settings.putLong(timestampKey(account), Clock.System.now().toEpochMilliseconds())
}
fun loadEvents(account: String): List<Event>? {
val jsonString = settings.getStringOrNull(eventsKey(account)) ?: return null
return try {
json.decodeFromString(eventListSerializer, jsonString)
} catch (e: Exception) {
null
}
}
fun getLastUpdated(account: String): Long? {
return settings.getLongOrNull(timestampKey(account))
}
fun saveFilters(account: String, agegroup: String?, subgroup: String?) {
if (agegroup != null) settings.putString(agegroupKey(account), agegroup)
else settings.remove(agegroupKey(account))
if (subgroup != null) settings.putString(subgroupKey(account), subgroup)
else settings.remove(subgroupKey(account))
}
fun loadFilters(account: String): Pair<String?, String?> {
return settings.getStringOrNull(agegroupKey(account)) to
settings.getStringOrNull(subgroupKey(account))
}
fun saveTypeFilter(account: String, typeFilter: String) {
settings.putString(typeFilterKey(account), typeFilter)
}
fun loadTypeFilter(account: String): String? {
return settings.getStringOrNull(typeFilterKey(account))
}
fun clearCache(account: String) {
settings.remove(eventsKey(account))
settings.remove(timestampKey(account))
settings.remove(agegroupKey(account))
settings.remove(subgroupKey(account))
settings.remove(typeFilterKey(account))
}
fun clearAllCache() {
val keys = settings.keys.filter { key ->
key.startsWith("cached_events_") ||
key.startsWith("filters_") ||
key.startsWith("cached_events_timestamp_")
}
keys.forEach { settings.remove(it) }
}
}
@@ -0,0 +1,121 @@
package ch.parano.myice.cache
import ch.parano.myice.models.Event
import com.russhwolf.settings.MapSettings
import com.russhwolf.settings.Settings
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class ScheduleCacheTest {
private fun createCache(): Pair<ScheduleCache, Settings> {
val settings = MapSettings()
return ScheduleCache(settings) to settings
}
private fun sampleEvent(id: String, eventType: String = "Jeu") = Event(
idEvent = id,
agegroup = "U13 (Elite)",
name = "U13 Elite - HC Ajoie",
title = "U13 (Elite)\nGame\nArena",
opponent = "HC Ajoie",
place = "Arena",
start = "2024-11-10T14:00:00",
end = "2024-11-10T16:15:00",
color = "#e4222e",
eventType = eventType,
)
@Test
fun testSaveAndLoadEvents() {
val (cache, _) = createCache()
val events = listOf(sampleEvent("1"), sampleEvent("2"))
cache.saveEvents("default", events)
val loaded = cache.loadEvents("default")
assertNotNull(loaded)
assertEquals(2, loaded.size)
assertEquals("1", loaded[0].idEvent)
assertEquals("2", loaded[1].idEvent)
}
@Test
fun testLoadEventsReturnsNullWhenEmpty() {
val (cache, _) = createCache()
assertNull(cache.loadEvents("default"))
}
@Test
fun testGetLastUpdated() {
val (cache, _) = createCache()
assertNull(cache.getLastUpdated("default"))
cache.saveEvents("default", listOf(sampleEvent("1")))
val timestamp = cache.getLastUpdated("default")
assertNotNull(timestamp)
assertTrue(timestamp > 0)
}
@Test
fun testSaveAndLoadFilters() {
val (cache, _) = createCache()
cache.saveFilters("default", agegroup = "U13 (Elite)", subgroup = "U13 Elite")
val (agegroup, subgroup) = cache.loadFilters("default")
assertEquals("U13 (Elite)", agegroup)
assertEquals("U13 Elite", subgroup)
}
@Test
fun testSaveFiltersWithNulls() {
val (cache, _) = createCache()
cache.saveFilters("default", agegroup = "U13", subgroup = "Elite")
cache.saveFilters("default", agegroup = null, subgroup = null)
val (agegroup, subgroup) = cache.loadFilters("default")
assertNull(agegroup)
assertNull(subgroup)
}
@Test
fun testClearCache() {
val (cache, _) = createCache()
cache.saveEvents("default", listOf(sampleEvent("1")))
cache.saveFilters("default", "U13", "Elite")
cache.clearCache("default")
assertNull(cache.loadEvents("default"))
val (agegroup, subgroup) = cache.loadFilters("default")
assertNull(agegroup)
assertNull(subgroup)
}
@Test
fun testClearAllCache() {
val (cache, _) = createCache()
cache.saveEvents("default", listOf(sampleEvent("1")))
cache.saveEvents("leonard", listOf(sampleEvent("2")))
cache.clearAllCache()
assertNull(cache.loadEvents("default"))
assertNull(cache.loadEvents("leonard"))
}
@Test
fun testTypeFilterPersistence() {
val (cache, _) = createCache()
cache.saveTypeFilter("default", "GAMES")
assertEquals("GAMES", cache.loadTypeFilter("default"))
}
@Test
fun testTypeFilterNullWhenNotSet() {
val (cache, _) = createCache()
assertNull(cache.loadTypeFilter("default"))
}
}