feat(Networking): enhance offline handling and localization for diary and members data
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 44s
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:
@@ -58,8 +58,8 @@ android {
|
||||
applicationId = "de.tsschulz.tt_tagebuch"
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.android.targetSdk.get().toInt()
|
||||
versionCode = 4
|
||||
versionName = "1.2.0"
|
||||
versionCode = libs.versions.appVersionCode.get().toInt()
|
||||
versionName = libs.versions.appVersionName.get()
|
||||
buildConfigField("String", "BACKEND_BASE_URL", "\"$backendBaseUrl\"")
|
||||
buildConfigField("String", "SOCKET_BASE_URL", "\"$socketBaseUrl\"")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application
|
||||
android:name=".app.MainApplication"
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.tsschulz.tt_tagebuch.app
|
||||
|
||||
import android.content.Context
|
||||
import de.tsschulz.tt_tagebuch.BuildConfig
|
||||
import de.tsschulz.tt_tagebuch.app.net.NetworkConnectivityHolder
|
||||
import de.tsschulz.tt_tagebuch.shared.api.BillingApi
|
||||
import de.tsschulz.tt_tagebuch.shared.api.CalendarHolidayApi
|
||||
import de.tsschulz.tt_tagebuch.shared.api.AccidentApi
|
||||
@@ -68,6 +69,7 @@ class AppDependencies(context: Context) {
|
||||
* (z. B. Club wählen → anderer Screen), im Gegensatz zu [rememberCoroutineScope].
|
||||
*/
|
||||
val applicationScope = CoroutineScope(applicationJob + Dispatchers.Main.immediate)
|
||||
val networkConnectivity = NetworkConnectivityHolder(context.applicationContext)
|
||||
val apiConfig = ApiConfig(baseUrl = BuildConfig.BACKEND_BASE_URL)
|
||||
val unauthorizedEvents = MutableStateFlow(0)
|
||||
private val tokenProvider = MutableTokenProvider()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.tsschulz.tt_tagebuch.app.net
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* Beobachtet die Standard-Netzwerkverbindung (z. B. für Reconnect-Refresh von Tagebuch/Mitgliedern).
|
||||
*/
|
||||
class NetworkConnectivityHolder(context: Context) {
|
||||
private val appContext = context.applicationContext
|
||||
private val cm = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
private val _connected = MutableStateFlow(readConnected())
|
||||
val connected: StateFlow<Boolean> = _connected.asStateFlow()
|
||||
|
||||
private val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
_connected.value = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
_connected.value = readConnected()
|
||||
}
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
_connected.value = readConnected()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
cm.registerDefaultNetworkCallback(callback)
|
||||
_connected.value = readConnected()
|
||||
}
|
||||
|
||||
private fun readConnected(): Boolean {
|
||||
val network = cm.activeNetwork ?: return false
|
||||
val caps = cm.getNetworkCapabilities(network) ?: return false
|
||||
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,40 @@ private fun MainTabs(dependencies: AppDependencies) {
|
||||
val useWideMainNav = LocalConfiguration.current.screenWidthDp >= MAIN_NAV_RAIL_MIN_WIDTH_DP
|
||||
val clubState by dependencies.clubManager.state.collectAsState()
|
||||
val visibleTabs = visibleMainTabs(clubState.currentPermissions)
|
||||
val networkConnected by dependencies.networkConnectivity.connected.collectAsState()
|
||||
var lastNetworkConnected by remember { mutableStateOf(true) }
|
||||
|
||||
/** Tagebuch- und Mitglieder-Listen vorladen, sobald Verein + Rechte feststehen. */
|
||||
LaunchedEffect(clubState.currentClubId, clubState.currentPermissions) {
|
||||
val id = clubState.currentClubId ?: return@LaunchedEffect
|
||||
val perms = clubState.currentPermissions ?: return@LaunchedEffect
|
||||
dependencies.applicationScope.launch {
|
||||
if (perms.canReadDiary()) {
|
||||
dependencies.diaryManager.loadDates(id)
|
||||
}
|
||||
if (perms.canReadMembers()) {
|
||||
dependencies.membersManager.loadMembers(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Nach Netzwerk-Wiederkehr Listen neu laden (Server ist Quelle der Wahrheit). */
|
||||
LaunchedEffect(networkConnected, clubState.currentClubId, clubState.currentPermissions) {
|
||||
val id = clubState.currentClubId ?: return@LaunchedEffect
|
||||
val perms = clubState.currentPermissions ?: return@LaunchedEffect
|
||||
val wasConnected = lastNetworkConnected
|
||||
lastNetworkConnected = networkConnected
|
||||
if (!wasConnected && networkConnected) {
|
||||
dependencies.applicationScope.launch {
|
||||
if (perms.canReadDiary()) {
|
||||
dependencies.diaryManager.loadDates(id)
|
||||
}
|
||||
if (perms.canReadMembers()) {
|
||||
dependencies.membersManager.loadMembers(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedTab) {
|
||||
if (selectedTab != MainTab.Settings) {
|
||||
@@ -1462,6 +1496,14 @@ private fun DiaryListScreen(
|
||||
.padding(horizontal = ScreenHorizontalPadding, vertical = 16.dp),
|
||||
) {
|
||||
Header(tr("diary.title", "Tagebuch"))
|
||||
if (diaryState.fromOfflineCache) {
|
||||
Text(
|
||||
tr("mobile.offlineCacheHint", "Keine Verbindung – zuletzt geladene Daten."),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.65f),
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
tr("diary.listHintCompact", "Trainingstag wählen und direkt bearbeiten."),
|
||||
style = MaterialTheme.typography.body2,
|
||||
@@ -4502,6 +4544,14 @@ private fun MembersScreen(
|
||||
.padding(horizontal = ScreenHorizontalPadding, vertical = 16.dp),
|
||||
) {
|
||||
Header(tr("members.title", "Mitglieder"))
|
||||
if (membersState.fromOfflineCache) {
|
||||
Text(
|
||||
trStr("mobile.offlineCacheHint", "Keine Verbindung – zuletzt geladene Daten."),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.65f),
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
||||
Reference in New Issue
Block a user