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:
parent
3f5a857c5a
commit
188bf01d6e
5 changed files with 60 additions and 12 deletions
10
android/app/src/debug/AndroidManifest.xml
Normal file
10
android/app/src/debug/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!--
|
||||
Debug builds only: allow plain-HTTP traffic so the app can talk to the
|
||||
local Docker backend (http://<lan-ip>:8000) during development.
|
||||
Android blocks cleartext traffic by default since API 28 — release
|
||||
builds intentionally do NOT get this override.
|
||||
-->
|
||||
<application android:usesCleartextTraffic="true" />
|
||||
</manifest>
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue