Fix infinite spinner on guide screen and allow cleartext HTTP in debug

Two bugs combined to cause this: (1) Android blocks plain-HTTP traffic
by default since API 28, so every call to our HTTP-only local backend
was throwing and being silently swallowed; (2) GuideViewModel's
loadNearby() returned early on that exception without ever setting
isLoading = false, so the spinner never cleared and no error showed.

Fix: debug-only manifest override (src/debug/AndroidManifest.xml)
enables cleartext traffic for local dev against the Docker backend;
GuideViewModel now always resolves isLoading and surfaces a
errorMessage + retry() the UI can act on instead of hanging forever.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
vrubelroman 2026-07-09 19:51:58 +00:00
parent 3f5a857c5a
commit 188bf01d6e
5 changed files with 60 additions and 12 deletions

View file

@ -1,13 +1,18 @@
package com.guidecity.app.ui.guide
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.VolumeOff
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -20,6 +25,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.guidecity.app.R
@ -61,6 +67,22 @@ fun GuideScreen(
.padding(padding),
) {
when {
state.errorMessage != null -> {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = state.errorMessage.orEmpty())
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.retry() }) {
Text(text = stringResource(R.string.guide_retry))
}
}
}
state.isLoading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()

View file

@ -26,6 +26,7 @@ data class GuideUiState(
val activeIndex: Int = 0,
val doneIndices: Set<Int> = emptySet(),
val isMuted: Boolean = false,
val errorMessage: String? = null,
)
/**
@ -73,11 +74,11 @@ class GuideViewModel @Inject constructor(
private suspend fun loadNearby(startNarration: Boolean) {
val location = selectedLocationHolder.location.value ?: locationProvider.getCurrentLocation()
if (location == null) {
_uiState.value = _uiState.value.copy(isLoading = false)
_uiState.value = _uiState.value.copy(isLoading = false, errorMessage = "Location unavailable")
return
}
val response = runCatching {
runCatching {
placesRepository.getNearby(
lat = location.lat,
lon = location.lon,
@ -85,17 +86,30 @@ class GuideViewModel @Inject constructor(
lang = "ru",
length = contentLength.toApiValue(),
)
}.getOrNull() ?: return
}.onSuccess { response ->
_uiState.value = _uiState.value.copy(
isLoading = false,
places = response.places,
activeIndex = 0,
doneIndices = emptySet(),
errorMessage = null,
)
if (startNarration) {
narrateActive()
}
}.onFailure { error ->
_uiState.value = _uiState.value.copy(
isLoading = false,
errorMessage = error.message ?: "Network error",
)
}
}
_uiState.value = _uiState.value.copy(
isLoading = false,
places = response.places,
activeIndex = 0,
doneIndices = emptySet(),
)
if (startNarration) {
narrateActive()
/** Retries after a failed load (e.g. tap a "retry" button shown on error). */
fun retry() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
loadNearby(startNarration = true)
}
}

View file

@ -17,6 +17,7 @@
<string name="map_search_placeholder">Введите место для поиска…</string>
<string name="guide_no_places_nearby">Поблизости пока не найдено интересных мест.</string>
<string name="guide_retry">Повторить</string>
<string name="guide_favorite_add">Добавить в избранное</string>
<string name="guide_favorite_remove">Убрать из избранного</string>
<string name="guide_mute">Выключить озвучку</string>

View file

@ -17,6 +17,7 @@
<string name="map_search_placeholder">Search for a place instead…</string>
<string name="guide_no_places_nearby">No places found nearby yet.</string>
<string name="guide_retry">Retry</string>
<string name="guide_favorite_add">Add to favorites</string>
<string name="guide_favorite_remove">Remove from favorites</string>
<string name="guide_mute">Mute narration</string>