fix(android): surface concert-fetch errors instead of swallowing them
Events were fetched with runCatching{}.getOrDefault(emptyList()), so any
network/deserialization failure looked identical to "no concerts". The
backend + Ticketmaster work (verified), so the failure is client-side.
- EventsClient: expectSuccess + timeouts so a non-2xx (e.g. an SSO/HTML
redirect) becomes a real exception instead of an obscure body() failure.
- ArtistViewModel: capture the failure into eventsError (+ Logcat) rather
than defaulting to empty.
- ConcertsSheet: show "Konzerte konnten nicht geladen werden" + the reason
when the fetch failed, vs. "Keine Konzerte angekündigt" when genuinely none.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b4599c50a
commit
f27eaf9c82
4 files changed
+26
-6
No files matched your search
@@ -4,6 +4,7 @@ import com.novasonic.app.api.model.ConcertEvent
|
|||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.call.*
|
import io.ktor.client.call.*
|
||||||
import io.ktor.client.engine.android.*
|
import io.ktor.client.engine.android.*
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
@@ -12,6 +13,10 @@ import kotlinx.serialization.json.Json
|
|||||||
object EventsClient {
|
object EventsClient {
|
||||||
|
|
||||||
private val http = HttpClient(Android) {
|
private val http = HttpClient(Android) {
|
||||||
|
// Surface non-2xx as a real exception (e.g. an SSO redirect / HTML login page) instead of
|
||||||
|
// letting .body() fail obscurely — the ViewModel turns it into a visible "load failed".
|
||||||
|
expectSuccess = true
|
||||||
|
install(HttpTimeout) { requestTimeoutMillis = 15_000; connectTimeoutMillis = 10_000 }
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json(Json { ignoreUnknownKeys = true; coerceInputValues = true })
|
json(Json { ignoreUnknownKeys = true; coerceInputValues = true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fun ConcertsSheet(
|
|||||||
artistName: String,
|
artistName: String,
|
||||||
events: List<ConcertEvent>,
|
events: List<ConcertEvent>,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
eventsError: String? = null,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -47,17 +48,25 @@ fun ConcertsSheet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (events.isEmpty()) {
|
if (events.isEmpty()) {
|
||||||
Box(
|
Column(
|
||||||
contentAlignment = Alignment.Center,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 40.dp),
|
.padding(horizontal = 20.dp, vertical = 40.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Keine Konzerte angekündigt",
|
if (eventsError != null) "Konzerte konnten nicht geladen werden" else "Keine Konzerte angekündigt",
|
||||||
fontFamily = Outfit, fontSize = 14.sp,
|
fontFamily = Outfit, fontSize = 14.sp,
|
||||||
color = NsOnSurface2,
|
color = NsOnSurface2,
|
||||||
)
|
)
|
||||||
|
if (eventsError != null) {
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
eventsError,
|
||||||
|
fontFamily = Outfit, fontSize = 11.sp,
|
||||||
|
color = NsOnSurface2.copy(alpha = 0.6f),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ fun ArtistDetailScreen(
|
|||||||
artistName = artist.name,
|
artistName = artist.name,
|
||||||
events = state.events,
|
events = state.events,
|
||||||
onDismiss = vm::dismissEventsSheet,
|
onDismiss = vm::dismissEventsSheet,
|
||||||
|
eventsError = state.eventsError,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ data class ArtistUiState(
|
|||||||
val partialAlbum: PartialAlbumState? = null,
|
val partialAlbum: PartialAlbumState? = null,
|
||||||
// Concerts
|
// Concerts
|
||||||
val events: List<ConcertEvent> = emptyList(),
|
val events: List<ConcertEvent> = emptyList(),
|
||||||
|
val eventsError: String? = null, // non-null → the fetch failed (vs. genuinely no concerts)
|
||||||
val showEventsSheet: Boolean = false,
|
val showEventsSheet: Boolean = false,
|
||||||
// Deezer fan count of the matched artist (shown under the name), null if unknown
|
// Deezer fan count of the matched artist (shown under the name), null if unknown
|
||||||
val fanCount: Long? = null,
|
val fanCount: Long? = null,
|
||||||
@@ -113,8 +114,12 @@ class ArtistViewModel(app: Application, savedState: SavedStateHandle) : AndroidV
|
|||||||
launch { loadItunesData(artist.name, pin) }
|
launch { loadItunesData(artist.name, pin) }
|
||||||
launch {
|
launch {
|
||||||
val backendUrl = repo.getBackendUrl()
|
val backendUrl = repo.getBackendUrl()
|
||||||
val ev = runCatching { EventsClient.fetchEvents(backendUrl, artist.name) }.getOrDefault(emptyList())
|
runCatching { EventsClient.fetchEvents(backendUrl, artist.name) }
|
||||||
_state.value = _state.value.copy(events = ev)
|
.onSuccess { _state.value = _state.value.copy(events = it, eventsError = null) }
|
||||||
|
.onFailure { e ->
|
||||||
|
android.util.Log.w("NovaSonic", "Konzert-Fetch fehlgeschlagen für ${artist.name}", e)
|
||||||
|
_state.value = _state.value.copy(events = emptyList(), eventsError = e.message ?: e.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_state.value = ArtistUiState(isLoading = false, itunesLoading = false, error = e.message ?: e.toString())
|
_state.value = ArtistUiState(isLoading = false, itunesLoading = false, error = e.message ?: e.toString())
|
||||||
|
|||||||
Reference in new issue
Block a user