Robust referensdata-hämtning + auto-retry i övningsväljaren
All checks were successful
release / build-release (push) Successful in 4m46s

Om servern saknar de nya fälten (iconKey/defaultRestSeconds) faller
queries tillbaka på den äldre formen istället för att lämna cachen tom.
Varje cachesektion hämtas oberoende. Väljaren triggar en ny hämtning när
cachen är tom och har en Uppdatera-knapp i tomläget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP
This commit is contained in:
2026-07-24 18:21:24 +02:00
parent 2c17873ca4
commit 93a1aa249a
3 changed files with 75 additions and 31 deletions

View File

@@ -144,39 +144,59 @@ class GymRepository(
private suspend fun refreshReferenceData() { private suspend fun refreshReferenceData() {
val token = auth.bearerToken() val token = auth.bearerToken()
val groups = client.execute("query{muscleGroups{id name iconKey}}", token = token) // Varje sektion för sig, och de nya API-fälten (iconKey,
db.cacheDao().upsertMuscleGroups( // defaultRestSeconds) med fallback — en äldre server ska aldrig
groups["muscleGroups"]!!.jsonArray.map { // kunna lämna hela cachen tom.
val o = it.jsonObject runCatching {
CachedMuscleGroup( val groups = try {
o["id"]!!.jsonPrimitive.int, client.execute("query{muscleGroups{id name iconKey}}", token = token)
o["name"]!!.jsonPrimitive.content, } catch (e: GraphQlException) {
o["iconKey"]?.jsonPrimitive?.contentOrNull(), client.execute("query{muscleGroups{id name}}", token = token)
)
} }
) db.cacheDao().upsertMuscleGroups(
groups["muscleGroups"]!!.jsonArray.map {
val o = it.jsonObject
CachedMuscleGroup(
o["id"]!!.jsonPrimitive.int,
o["name"]!!.jsonPrimitive.content,
o["iconKey"]?.jsonPrimitive?.contentOrNull(),
)
}
)
}
val muscles = client.execute("query{muscles{id name muscleGroupId}}", token = token) runCatching {
db.cacheDao().upsertMuscles( val muscles = client.execute("query{muscles{id name muscleGroupId}}", token = token)
muscles["muscles"]!!.jsonArray.map { db.cacheDao().upsertMuscles(
val o = it.jsonObject muscles["muscles"]!!.jsonArray.map {
CachedMuscle( val o = it.jsonObject
o["id"]!!.jsonPrimitive.int, CachedMuscle(
o["name"]!!.jsonPrimitive.content, o["id"]!!.jsonPrimitive.int,
o["muscleGroupId"]?.jsonPrimitive?.intOrNull, o["name"]!!.jsonPrimitive.content,
) o["muscleGroupId"]?.jsonPrimitive?.intOrNull,
} )
) }
)
}
val favIds: Set<Int> = runCatching { val favIds: Set<Int> = runCatching {
client.execute("query{favoriteExercises{id}}", token = token)["favoriteExercises"]!! client.execute("query{favoriteExercises{id}}", token = token)["favoriteExercises"]!!
.jsonArray.map { it.jsonObject["id"]!!.jsonPrimitive.int }.toSet() .jsonArray.map { it.jsonObject["id"]!!.jsonPrimitive.int }.toSet()
}.getOrDefault(emptySet()) }.getOrDefault(emptySet())
val types = client.execute( val typeFields =
"query{exerciseTypes{id name description metValue tracksWeight tracksReps tracksDistance tracksDuration isBodyweight defaultRestSeconds exerciseTypeMuscles{muscle{id}}}}", "id name description metValue tracksWeight tracksReps tracksDistance tracksDuration isBodyweight"
token = token, val types = try {
) client.execute(
"query{exerciseTypes{$typeFields defaultRestSeconds exerciseTypeMuscles{muscle{id}}}}",
token = token,
)
} catch (e: GraphQlException) {
client.execute(
"query{exerciseTypes{$typeFields exerciseTypeMuscles{muscle{id}}}}",
token = token,
)
}
db.cacheDao().replaceExerciseTypes( db.cacheDao().replaceExerciseTypes(
types["exerciseTypes"]!!.jsonArray.map { types["exerciseTypes"]!!.jsonArray.map {
val o = it.jsonObject val o = it.jsonObject

View File

@@ -180,12 +180,26 @@ fun ExercisePickerScreen(
} }
if (uiState.exercises.isEmpty()) { if (uiState.exercises.isEmpty()) {
item { item {
Text( Column(
"Inga övningar matchar — testa ett annat filter.", modifier = Modifier
style = MaterialTheme.typography.bodyMedium, .fillMaxWidth()
color = MaterialTheme.colorScheme.onSurfaceVariant, .padding(16.dp),
modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally,
) ) {
Text(
if (uiState.query.isBlank() && uiState.selectedGroupId == null) {
"Övningslistan är tom — hämtar från servern …"
} else {
"Inga övningar matchar — testa ett annat filter."
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
androidx.compose.material3.FilledTonalButton(
onClick = viewModel::refresh,
modifier = Modifier.padding(top = 12.dp),
) { Text("Uppdatera från servern") }
}
} }
} }
} }

View File

@@ -43,6 +43,16 @@ class PickerViewModel(private val repo: GymRepository) : ViewModel() {
private val selectedGroup = MutableStateFlow<Int?>(null) private val selectedGroup = MutableStateFlow<Int?>(null)
private val selectedMuscle = MutableStateFlow<Int?>(null) private val selectedMuscle = MutableStateFlow<Int?>(null)
init {
// Tom cache (t.ex. första start eller misslyckad förra hämtning)?
// Försök fylla den direkt när väljaren öppnas.
viewModelScope.launch {
if (repo.exerciseTypes.first().isEmpty()) repo.refreshAllAsync()
}
}
fun refresh() = repo.refreshAllAsync()
private data class Filters(val query: String, val groupId: Int?, val muscleId: Int?) private data class Filters(val query: String, val groupId: Int?, val muscleId: Int?)
private val filters = combine(query, selectedGroup, selectedMuscle) { q, g, m -> Filters(q, g, m) } private val filters = combine(query, selectedGroup, selectedMuscle) { q, g, m -> Filters(q, g, m) }