feat(Networking): enhance offline handling and localization for diary and members data
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s

- Added `ACCESS_NETWORK_STATE` permission in AndroidManifest.xml to monitor network connectivity.
- Introduced `NetworkConnectivityHolder` to manage network state and trigger data refresh when connectivity is restored.
- Updated `DiaryManager` and `MembersManager` to support offline caching, allowing users to view previously loaded data when offline.
- Enhanced localization by adding new keys for offline cache hints in both German and English, improving user experience during connectivity issues.
- Updated UI components to display offline cache messages, ensuring users are informed when data is being served from cache.
This commit is contained in:
Torsten Schulz (local)
2026-05-15 08:10:01 +02:00
parent 2f15827658
commit 320010b94e
12 changed files with 198 additions and 8 deletions

View File

@@ -1341,6 +1341,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Keine Verbindung zuletzt geladene Daten.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -3853,6 +3854,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Keine Verbindung zuletzt geladene Daten.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -6366,6 +6368,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Keine Verbindung zuletzt geladene Daten.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -8872,6 +8875,7 @@ object MobileStrings {
"mobile.noMembers" to "No members found",
"mobile.noResults" to "No results",
"mobile.noTimes" to "No times",
"mobile.offlineCacheHint" to "No connection showing last loaded data.",
"mobile.participationTop" to "Top attendance",
"mobile.refresh" to "Refresh",
"mobile.requestedAccess" to "Requested",
@@ -11378,6 +11382,7 @@ object MobileStrings {
"mobile.noMembers" to "No members found",
"mobile.noResults" to "No results",
"mobile.noTimes" to "No times",
"mobile.offlineCacheHint" to "No connection showing last loaded data.",
"mobile.participationTop" to "Top attendance",
"mobile.refresh" to "Refresh",
"mobile.requestedAccess" to "Requested",
@@ -13884,6 +13889,7 @@ object MobileStrings {
"mobile.noMembers" to "No members found",
"mobile.noResults" to "No results",
"mobile.noTimes" to "No times",
"mobile.offlineCacheHint" to "No connection showing last loaded data.",
"mobile.participationTop" to "Top attendance",
"mobile.refresh" to "Refresh",
"mobile.requestedAccess" to "Requested",
@@ -16390,6 +16396,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Sin conexión: se muestran los datos cargados por última vez.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -18896,6 +18903,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Walang koneksyon ipinapakita ang huling na-load na datos.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -21402,6 +21410,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Pas de connexion affichage des données du dernier chargement.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -23908,6 +23917,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Nessuna connessione dati dellultimo caricamento.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -26414,6 +26424,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "ネットワークに接続されていません。最後に読み込んだデータを表示しています。",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -28920,6 +28931,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Brak połączenia wyświetlane ostatnio wczytane dane.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -31426,6 +31438,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "ไม่มีการเชื่อมต่อ แสดงข้อมูลที่โหลดล่าสุด",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -33932,6 +33945,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "Walang koneksyon ipinapakita ang huling na-load na datos.",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",
@@ -36438,6 +36452,7 @@ object MobileStrings {
"mobile.noMembers" to "Keine Mitglieder gefunden",
"mobile.noResults" to "Keine Treffer",
"mobile.noTimes" to "Keine Zeiten",
"mobile.offlineCacheHint" to "无网络连接,显示上次加载的数据。",
"mobile.participationTop" to "Top Teilnahmen",
"mobile.refresh" to "Aktualisieren",
"mobile.requestedAccess" to "Angefragt",

View File

@@ -224,12 +224,32 @@ class DiaryManager(
}
suspend fun loadDates(clubId: Int) {
_state.value = _state.value.copy(isLoading = true, error = null)
val prev = _state.value
val base = if (prev.lastLoadedClubId != null && prev.lastLoadedClubId != clubId) {
prev.copy(dates = emptyList(), lastLoadedClubId = null, fromOfflineCache = false, error = null)
} else {
prev
}
_state.value = base.copy(isLoading = true, error = null)
try {
val dates = diaryApi.listDates(clubId)
_state.value = _state.value.copy(isLoading = false, dates = dates)
_state.value = base.copy(
isLoading = false,
dates = dates,
lastLoadedClubId = clubId,
fromOfflineCache = false,
error = null,
)
} catch (t: Throwable) {
_state.value = _state.value.copy(isLoading = false, error = t.toUserMessage("Fehler beim Laden des Tagebuchs"))
if (base.lastLoadedClubId == clubId && base.dates.isNotEmpty()) {
_state.value = base.copy(isLoading = false, fromOfflineCache = true, error = null)
} else {
_state.value = base.copy(
isLoading = false,
error = t.toUserMessage("Fehler beim Laden des Tagebuchs"),
fromOfflineCache = false,
)
}
}
}

View File

@@ -6,5 +6,9 @@ data class DiaryState(
val dates: List<DiaryDate> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
/** Zuletzt erfolgreich geladener Verein (für Offline-Cache). */
val lastLoadedClubId: Int? = null,
/** `true`, wenn die angezeigte Tagebuch-Liste von einem fehlgeschlagenen Refresh stammt. */
val fromOfflineCache: Boolean = false,
)

View File

@@ -32,7 +32,13 @@ class MembersManager(
val state: StateFlow<MembersState> = _state.asStateFlow()
suspend fun loadMembers(clubId: Int) {
_state.value = _state.value.copy(isLoading = true, error = null)
val prev = _state.value
val base = if (prev.lastLoadedClubId != null && prev.lastLoadedClubId != clubId) {
prev.copy(members = emptyList(), lastLoadedClubId = null, fromOfflineCache = false, error = null)
} else {
prev
}
_state.value = base.copy(isLoading = true, error = null)
try {
val members = membersApi.listMembers(clubId)
val merged = runCatching { trainingStatsApi.getStats(clubId) }
@@ -41,9 +47,23 @@ class MembersManager(
onFailure = { members },
)
.sortedWith(compareBy<Member> { it.lastName.lowercase() }.thenBy { it.firstName.lowercase() })
_state.value = _state.value.copy(members = merged, isLoading = false)
_state.value = base.copy(
members = merged,
isLoading = false,
lastLoadedClubId = clubId,
fromOfflineCache = false,
error = null,
)
} catch (t: Throwable) {
_state.value = _state.value.copy(isLoading = false, error = t.toUserMessage("Mitglieder konnten nicht geladen werden"))
if (base.lastLoadedClubId == clubId && base.members.isNotEmpty()) {
_state.value = base.copy(isLoading = false, fromOfflineCache = true, error = null)
} else {
_state.value = base.copy(
isLoading = false,
error = t.toUserMessage("Mitglieder konnten nicht geladen werden"),
fromOfflineCache = false,
)
}
}
}

View File

@@ -6,5 +6,7 @@ data class MembersState(
val members: List<Member> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
val lastLoadedClubId: Int? = null,
val fromOfflineCache: Boolean = false,
)