feat: add ScheduleCache with multiplatform-settings for offline caching
This commit is contained in:
@@ -36,6 +36,7 @@ kotlin {
|
|||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.multiplatform.settings)
|
implementation(libs.multiplatform.settings)
|
||||||
implementation(libs.multiplatform.settings.no.arg)
|
implementation(libs.multiplatform.settings.no.arg)
|
||||||
implementation(libs.lifecycle.viewmodel.compose)
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ agp = "8.13.2"
|
|||||||
ktor = "3.5.1"
|
ktor = "3.5.1"
|
||||||
kotlinx-serialization-json = "1.11.0"
|
kotlinx-serialization-json = "1.11.0"
|
||||||
kotlinx-coroutines = "1.11.0"
|
kotlinx-coroutines = "1.11.0"
|
||||||
|
kotlinx-datetime = "0.7.0"
|
||||||
multiplatform-settings = "1.3.0"
|
multiplatform-settings = "1.3.0"
|
||||||
lifecycle-viewmodel-compose = "2.10.0"
|
lifecycle-viewmodel-compose = "2.10.0"
|
||||||
androidx-activity-compose = "1.13.0"
|
androidx-activity-compose = "1.13.0"
|
||||||
@@ -19,6 +20,7 @@ ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "kto
|
|||||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" }
|
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" }
|
||||||
multiplatform-settings-no-arg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" }
|
multiplatform-settings-no-arg = { module = "com.russhwolf:multiplatform-settings-no-arg", version.ref = "multiplatform-settings" }
|
||||||
multiplatform-settings-test = { module = "com.russhwolf:multiplatform-settings-test", version.ref = "multiplatform-settings" }
|
multiplatform-settings-test = { module = "com.russhwolf:multiplatform-settings-test", version.ref = "multiplatform-settings" }
|
||||||
|
|||||||
Reference in New Issue
Block a user