Implement network retry mechanism across repositories and add connectivity monitoring
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 5m39s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

- Introduced `retryOnNetworkFailure` function to handle network-related exceptions and retry requests.
- Updated `GalleryRepository`, `HomeRepository`, `LoginRepository`, `MannschaftenRepository`, `NewsletterRepository`, `PasskeyRepository`, `ProfileRepository`, `PublicPagesRepository`, `SpielplanRepository`, `TermineRepository`, and `TrainingRepository` to use the new retry mechanism.
- Added `ConnectivityMonitor` to track internet connectivity status and notify UI components.
- Enhanced `NavigationViewModel`, `CmsViewModel`, `MembersViewModel`, and `MemberAreaViewModel` to reload data when connectivity is restored.
- Bumped app version to 0.9.16.
This commit is contained in:
Torsten Schulz (local)
2026-06-04 22:15:44 +02:00
parent 402913d877
commit e517720b03
19 changed files with 419 additions and 205 deletions

View File

@@ -0,0 +1,49 @@
package de.harheimertc.data
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ConnectivityMonitor @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _online = MutableStateFlow(hasInternetAccess())
val online: StateFlow<Boolean> = _online.asStateFlow()
init {
scope.launch { poll() }
}
private suspend fun poll() {
while (currentCoroutineContext().isActive) {
val current = hasInternetAccess()
if (_online.value != current) {
_online.value = current
}
delay(10_000L)
}
}
private fun hasInternetAccess(): Boolean {
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
?: return false
val network = manager.activeNetwork ?: return false
val capabilities = manager.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}

View File

@@ -24,27 +24,27 @@ class GalleryRepository @Inject constructor(
@param:ApplicationContext private val context: Context,
) {
suspend fun hasPublicImages(): Result<Boolean> = runCatching {
retryOnNetworkFailure {
val response = api.galerieList(page = 1, perPage = 1)
if (!response.isSuccessful) error("HTTP ${response.code()}")
response.body()?.images.orEmpty().isNotEmpty()
}
}
suspend fun fetchImages(page: Int = 1, perPage: Int = 60): Result<GalleryPage> {
return try {
return runCatching {
retryOnNetworkFailure {
val resp = api.galerieList(page = page, perPage = perPage)
if (resp.isSuccessful) {
val body = resp.body()
Result.success(
GalleryPage(
images = body?.images.orEmpty().map { it.toGalleryImage() },
pagination = body?.pagination ?: GalleryPaginationDto(),
),
)
} else {
Result.failure(Exception("HTTP ${resp.code()}"))
error("HTTP ${resp.code()}")
}
}
} catch (e: Exception) {
Result.failure(e)
}
}

View File

@@ -31,6 +31,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
val diagnostics = mutableListOf<String>()
val termine = runCatching {
retryOnNetworkFailure {
val response = api.termine()
if (!response.isSuccessful) {
val errorBody = response.errorBody()?.string().orEmpty()
@@ -44,6 +45,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
error("Termine konnten nicht geladen werden (HTTP ${response.code()}).")
}
response.body()?.termine.orEmpty()
}
}.onFailure { error ->
captureLoadIssue("fetchHomeData.termine", error)
if (diagnostics.none { it.contains("GET /api/termine") }) {
@@ -58,6 +60,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
}.getOrDefault(emptyList())
val spielplanResponse = runCatching {
retryOnNetworkFailure {
val response = api.spielplan()
if (!response.isSuccessful) {
val errorBody = response.errorBody()?.string().orEmpty()
@@ -71,6 +74,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
error("Spielplan konnte nicht geladen werden (HTTP ${response.code()}).")
}
response.body()
}
}.onFailure { error ->
captureLoadIssue("fetchHomeData.spielplan", error)
if (diagnostics.none { it.contains("GET /api/spielplan") }) {
@@ -86,6 +90,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
val spiele = spielplanResponse?.data.orEmpty()
val news = runCatching {
retryOnNetworkFailure {
val response = api.publicNews()
if (!response.isSuccessful) {
val errorBody = response.errorBody()?.string().orEmpty()
@@ -99,6 +104,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
error("News konnten nicht geladen werden (HTTP ${response.code()}).")
}
response.body()?.news.orEmpty()
}
}.onFailure { error ->
captureLoadIssue("fetchHomeData.news", error)
if (diagnostics.none { it.contains("GET /api/news-public") }) {
@@ -113,6 +119,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
}.getOrDefault(emptyList())
val homepageSections = runCatching {
retryOnNetworkFailure {
val response = api.config()
if (!response.isSuccessful) {
val errorBody = response.errorBody()?.string().orEmpty()
@@ -126,6 +133,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
error("Konfiguration konnte nicht geladen werden (HTTP ${response.code()}).")
}
response.body()?.homepage?.sections.orEmpty()
}
}.onFailure { error ->
captureLoadIssue("fetchHomeData.config", error)
if (diagnostics.none { it.contains("GET /api/config") }) {
@@ -140,6 +148,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
}.getOrDefault(emptyList())
val heroImageUrl = runCatching {
retryOnNetworkFailure {
val response = api.heroImages()
if (!response.isSuccessful) {
val errorBody = response.errorBody()?.string().orEmpty()
@@ -154,6 +163,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
}
val variants = response.body()?.variants.orEmpty()
pickRandomHeroImage(variants)
}
}.onFailure { error ->
captureLoadIssue("fetchHomeData.heroImages", error)
if (diagnostics.none { it.contains("GET /api/hero-images") }) {
@@ -187,9 +197,11 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
}
suspend fun fetchSpielplanForSeason(season: String): Result<SpielplanResponse> = runCatching {
retryOnNetworkFailure {
val response = api.spielplan(season)
if (!response.isSuccessful) error("HTTP ${response.code()}")
response.body() ?: error("Leere Antwort")
}
}.onFailure { error ->
Sentry.withScope { scope ->
scope.setTag("repository", "HomeRepository")

View File

@@ -23,7 +23,7 @@ class LoginRepository @Inject constructor(
suspend fun login(email: String, password: String): Result<LoginResponse> = runCatching {
val endpoint = "api/auth/login"
val requestPreview = "{email=\"${maskEmail(email)}\", client=\"android\", deviceName=\"Harheimer TC Android-App\"}"
val response = api.login(LoginRequest(email.trim(), password))
val response = retryOnNetworkFailure { api.login(LoginRequest(email.trim(), password)) }
if (!response.isSuccessful) {
val body = response.errorBody()?.string().orEmpty()
val serverMessage = extractServerMessage(body)
@@ -71,11 +71,11 @@ class LoginRepository @Inject constructor(
return@runCatching AuthStatusResponse()
}
var response = api.authStatus()
var response = retryOnNetworkFailure { api.authStatus() }
if (!response.isSuccessful) error("Status konnte nicht geprüft werden.")
var status = response.body() ?: AuthStatusResponse()
if (!status.isLoggedIn && authRepository.getRefreshToken() != null && sessionRefresher.refreshAccessToken()) {
response = api.authStatus()
response = retryOnNetworkFailure { api.authStatus() }
if (!response.isSuccessful) error("Status konnte nicht geprüft werden.")
status = response.body() ?: AuthStatusResponse()
}
@@ -93,16 +93,20 @@ class LoginRepository @Inject constructor(
}
suspend fun resetPassword(email: String): Result<AuthMessageResponse> = runCatching {
retryOnNetworkFailure {
val response = api.resetPassword(ResetPasswordRequest(email.trim()))
if (!response.isSuccessful) error("Anfrage konnte nicht gesendet werden.")
response.body() ?: error("Leere Antwort")
}
}
suspend fun register(request: RegistrationRequest): Result<AuthMessageResponse> = runCatching {
retryOnNetworkFailure {
val response = api.register(request)
if (!response.isSuccessful) error("Registrierung fehlgeschlagen.")
response.body() ?: error("Leere Antwort")
}
}
private fun extractServerMessage(raw: String): String? {
if (raw.isBlank()) return null

View File

@@ -26,16 +26,20 @@ data class Mannschaft(
@Singleton
class MannschaftenRepository @Inject constructor(private val api: ApiService) {
suspend fun fetchMannschaften(season: String? = null): Result<List<Mannschaft>> = runCatching {
retryOnNetworkFailure {
val response = api.mannschaften(season)
if (!response.isSuccessful) error("Mannschaften konnten nicht geladen werden.")
parseCsv(response.body()?.string().orEmpty())
}
}
suspend fun fetchSeasons(): Result<MannschaftenSeasonsResponse> = runCatching {
retryOnNetworkFailure {
val response = api.mannschaftenSeasons()
if (!response.isSuccessful) error("Saisons konnten nicht geladen werden.")
response.body() ?: error("Saisons konnten nicht geladen werden.")
}
}
private fun parseCsv(csv: String): List<Mannschaft> = csv.lineSequence()
.filter(String::isNotBlank)

View File

@@ -0,0 +1,33 @@
package de.harheimertc.repositories
import java.net.ConnectException
import java.net.NoRouteToHostException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.net.ssl.SSLException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
internal suspend fun <T> retryOnNetworkFailure(
retryDelayMillis: Long = 10_000L,
block: suspend () -> T,
): T {
while (true) {
try {
return block()
} catch (error: Throwable) {
if (error is CancellationException) throw error
if (!error.isRetryableNetworkError()) throw error
delay(retryDelayMillis)
}
}
}
private fun Throwable.isRetryableNetworkError(): Boolean = when (this) {
is UnknownHostException,
is ConnectException,
is NoRouteToHostException,
is SocketTimeoutException,
is SSLException -> true
else -> false
}

View File

@@ -8,10 +8,12 @@ import javax.inject.Inject
class NewsletterRepository @Inject constructor(private val api: ApiService) {
suspend fun groups(): Result<NewsletterGroupsResponse> = runCatching {
retryOnNetworkFailure {
val response = api.publicNewsletterGroups()
if (!response.isSuccessful) error("Newsletter konnten nicht geladen werden.")
response.body() ?: error("Leere Antwort vom Server.")
}
}
suspend fun subscribe(groupId: String, email: String, name: String?): Result<AuthMessageResponse> = runCatching {
val response = api.subscribeNewsletter(NewsletterSubscriptionRequest(groupId, email.trim(), name?.trim().orEmpty()))
@@ -26,8 +28,10 @@ class NewsletterRepository @Inject constructor(private val api: ApiService) {
}
suspend fun confirm(token: String): Result<AuthMessageResponse> = runCatching {
retryOnNetworkFailure {
val response = api.confirmNewsletter(token)
if (!response.isSuccessful) error("Newsletter-Bestätigung fehlgeschlagen.")
response.body() ?: AuthMessageResponse(success = true, message = "Newsletter-Anmeldung bestätigt.")
}
}
}

View File

@@ -28,6 +28,7 @@ class PasskeyRepository @Inject constructor(
private val authRepository: AuthRepository,
) {
suspend fun login(context: Context, email: String?): Result<LoginResponse> = runCatching {
retryOnNetworkFailure {
val optionsResponse = api.passkeyAuthenticationOptions(
PasskeyAuthenticationOptionsRequest(email = email?.trim()?.takeIf(String::isNotBlank)),
)
@@ -59,15 +60,19 @@ class PasskeyRepository @Inject constructor(
?: error("Der Server hat kein Zugriffstoken geliefert.")
authRepository.setSession(token, body.refreshToken, body.sessionId)
body
}
}.recoverCredentialCancellation("Passkey-Anmeldung abgebrochen.")
suspend fun list(): Result<PasskeysResponse> = runCatching {
retryOnNetworkFailure {
val response = api.passkeys()
if (!response.isSuccessful) error("Passkeys konnten nicht geladen werden.")
response.body() ?: error("Leere Antwort")
}
}
suspend fun add(context: Context, name: String = "Android-App"): Result<AuthMessageResponse> = runCatching {
retryOnNetworkFailure {
val optionsResponse = api.passkeyRegistrationOptions(PasskeyRegistrationOptionsRequest())
if (!optionsResponse.isSuccessful) error("Passkey-Erstellung konnte nicht gestartet werden.")
val optionsJson = optionsResponse.body()?.string()?.extractJsonObject("options")
@@ -90,6 +95,7 @@ class PasskeyRepository @Inject constructor(
)
if (!response.isSuccessful) error("Passkey konnte nicht hinzugefügt werden.")
response.body() ?: error("Leere Antwort")
}
}.recoverCredentialCancellation("Passkey-Erstellung abgebrochen.")
suspend fun remove(credentialId: String): Result<AuthMessageResponse> = runCatching {

View File

@@ -9,14 +9,18 @@ import javax.inject.Singleton
@Singleton
class ProfileRepository @Inject constructor(private val api: ApiService) {
suspend fun load(): Result<ProfileResponse> = runCatching {
retryOnNetworkFailure {
val response = api.profile()
if (!response.isSuccessful) error("Profil konnte nicht geladen werden.")
response.body() ?: error("Leere Antwort")
}
}
suspend fun save(request: ProfileUpdateRequest): Result<ProfileResponse> = runCatching {
retryOnNetworkFailure {
val response = api.updateProfile(request)
if (!response.isSuccessful) error("Profil konnte nicht gespeichert werden.")
response.body() ?: error("Leere Antwort")
}
}
}

View File

@@ -31,12 +31,15 @@ data class MeisterschaftResult(
@Singleton
class PublicPagesRepository @Inject constructor(private val api: ApiService) {
suspend fun fetchConfig(): Result<ConfigResponse> = runCatching {
retryOnNetworkFailure {
val response = api.config()
if (!response.isSuccessful) error("Inhalte konnten nicht geladen werden.")
response.body() ?: error("Leere Antwort")
}
}
suspend fun fetchSpielsysteme(): Result<List<Spielsystem>> = runCatching {
retryOnNetworkFailure {
val response = api.spielsysteme()
if (!response.isSuccessful) error("Spielsysteme konnten nicht geladen werden.")
parseCsv(response.body()?.string().orEmpty()).drop(1).mapNotNull { values ->
@@ -52,8 +55,10 @@ class PublicPagesRepository @Inject constructor(private val api: ApiService) {
)
}
}
}
suspend fun fetchVereinsmeisterschaften(): Result<List<MeisterschaftResult>> = runCatching {
retryOnNetworkFailure {
val response = api.vereinsmeisterschaften()
if (!response.isSuccessful) error("Vereinsmeisterschaften konnten nicht geladen werden.")
parseCsv(response.body()?.string().orEmpty()).drop(1).mapNotNull { values ->
@@ -71,6 +76,7 @@ class PublicPagesRepository @Inject constructor(private val api: ApiService) {
}
}
}
}
private fun parseCsv(csv: String): List<List<String>> =
csv.lineSequence().filter(String::isNotBlank).map(::parseCsvLine).toList()

View File

@@ -9,14 +9,17 @@ import javax.inject.Singleton
@Singleton
class SpielplanRepository @Inject constructor(private val api: ApiService) {
suspend fun fetchSpielplan(season: String? = null): Result<SpielplanResponse> = runCatching {
retryOnNetworkFailure {
val response = api.spielplan(season)
if (!response.isSuccessful) error("HTTP ${response.code()}")
val body = response.body() ?: error("Leere Antwort")
if (!body.success) error(body.message ?: "Spielplan konnte nicht geladen werden.")
body
}
}
suspend fun fetchTeamTable(team: String, season: String? = null): Result<TeamTableResponse> = runCatching {
retryOnNetworkFailure {
val response = api.spielplanTable(team, season)
if (!response.isSuccessful) error("HTTP ${response.code()}")
val body = response.body() ?: error("Leere Antwort")
@@ -24,3 +27,4 @@ class SpielplanRepository @Inject constructor(private val api: ApiService) {
body
}
}
}

View File

@@ -8,8 +8,10 @@ import javax.inject.Singleton
@Singleton
class TermineRepository @Inject constructor(private val api: ApiService) {
suspend fun fetchTermine(): Result<List<TerminDto>> = runCatching {
retryOnNetworkFailure {
val response = api.termine()
if (!response.isSuccessful) error("HTTP ${response.code()}")
response.body()?.termine.orEmpty()
}
}
}

View File

@@ -8,8 +8,10 @@ import javax.inject.Singleton
@Singleton
class TrainingRepository @Inject constructor(private val api: ApiService) {
suspend fun fetchConfig(): Result<ConfigResponse> = runCatching {
retryOnNetworkFailure {
val response = api.config()
if (!response.isSuccessful) error("Trainingsinformationen konnten nicht geladen werden.")
response.body() ?: error("Leere Antwort")
}
}
}

View File

@@ -2,10 +2,16 @@ package de.harheimertc.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
@@ -35,6 +41,16 @@ fun NavGraph(
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val persistentNavigation = maxWidth >= 600.dp
Column(modifier = Modifier.fillMaxSize()) {
navigationState.connectionNote?.let { message ->
Surface(color = Color(0xFFFFF4E5), modifier = Modifier.fillMaxWidth()) {
Text(
text = message,
color = Color(0xFF7C2D12),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
)
}
}
if (persistentNavigation) {
AppNavigationHeader(
selectedRoute = currentRoute,

View File

@@ -3,6 +3,7 @@ package de.harheimertc.ui.navigation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.harheimertc.data.ConnectivityMonitor
import de.harheimertc.repositories.AuthRepository
import de.harheimertc.repositories.GalleryRepository
import de.harheimertc.repositories.LoginRepository
@@ -19,6 +20,7 @@ data class NavigationUiState(
val hasGalleryImages: Boolean = false,
val loggedIn: Boolean = false,
val roles: Set<String> = emptySet(),
val connectionNote: String? = null,
) {
val isAdmin: Boolean get() = "admin" in roles
val canAccessNewsletter: Boolean get() = roles.any { it in setOf("admin", "vorstand", "newsletter") }
@@ -32,12 +34,22 @@ class NavigationViewModel @Inject constructor(
private val galleryRepository: GalleryRepository,
private val loginRepository: LoginRepository,
private val authRepository: AuthRepository,
private val connectivityMonitor: ConnectivityMonitor,
) : ViewModel() {
private val _state = MutableStateFlow(NavigationUiState())
val state: StateFlow<NavigationUiState> = _state
init {
loadNavigationData()
viewModelScope.launch {
var wasOnline: Boolean? = null
connectivityMonitor.online.collect { online ->
_state.value = _state.value.copy(
connectionNote = if (online) null else "Keine Verbindung. Die App versucht alle 10 Sekunden erneut zu laden.",
)
wasOnline = online
}
}
}
fun loadNavigationData() {
@@ -52,6 +64,7 @@ class NavigationViewModel @Inject constructor(
hasGalleryImages = gallery.await(),
loggedIn = hasStoredSession || status.isLoggedIn,
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
connectionNote = null,
)
}
}
@@ -63,6 +76,7 @@ class NavigationViewModel @Inject constructor(
_state.value = _state.value.copy(
loggedIn = hasStoredSession || status.isLoggedIn,
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
connectionNote = _state.value.connectionNote,
)
}
}
@@ -73,6 +87,7 @@ class NavigationViewModel @Inject constructor(
_state.value = _state.value.copy(
loggedIn = false,
roles = emptySet(),
connectionNote = _state.value.connectionNote,
)
onComplete()
}

View File

@@ -3,6 +3,7 @@ package de.harheimertc.ui.screens.cms
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.harheimertc.data.ConnectivityMonitor
import de.harheimertc.data.CmsUserDto
import de.harheimertc.data.ConfigResponse
import de.harheimertc.data.ContactRequestDto
@@ -43,12 +44,22 @@ data class CmsUiState(
@HiltViewModel
class CmsViewModel @Inject constructor(
private val repository: CmsRepository,
private val connectivityMonitor: ConnectivityMonitor,
) : ViewModel() {
private val _state = MutableStateFlow(CmsUiState())
val state: StateFlow<CmsUiState> = _state
init {
load()
viewModelScope.launch {
var wasOnline: Boolean? = null
connectivityMonitor.online.collect { online ->
if (online && wasOnline == false) {
load()
}
wasOnline = online
}
}
}
fun load() {

View File

@@ -3,6 +3,7 @@ package de.harheimertc.ui.screens.memberarea
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.harheimertc.data.ConnectivityMonitor
import de.harheimertc.data.AuthStatusResponse
import de.harheimertc.data.MemberDto
import de.harheimertc.data.NewsDto
@@ -24,12 +25,22 @@ data class MembersUiState(
@HiltViewModel
class MembersViewModel @Inject constructor(
private val repository: MemberAreaRepository,
private val connectivityMonitor: ConnectivityMonitor,
) : ViewModel() {
private val _state = MutableStateFlow(MembersUiState())
val state: StateFlow<MembersUiState> = _state
init {
load()
viewModelScope.launch {
var wasOnline: Boolean? = null
connectivityMonitor.online.collect { online ->
if (online && wasOnline == false) {
load()
}
wasOnline = online
}
}
}
fun updateQuery(query: String) {
@@ -87,12 +98,22 @@ data class MemberNewsUiState(
@HiltViewModel
class MemberNewsViewModel @Inject constructor(
private val repository: MemberAreaRepository,
private val connectivityMonitor: ConnectivityMonitor,
) : ViewModel() {
private val _state = MutableStateFlow(MemberNewsUiState())
val state: StateFlow<MemberNewsUiState> = _state
init {
load()
viewModelScope.launch {
var wasOnline: Boolean? = null
connectivityMonitor.online.collect { online ->
if (online && wasOnline == false) {
load()
}
wasOnline = online
}
}
}
fun load() {
@@ -118,12 +139,22 @@ data class QttrUiState(
class QttrViewModel @Inject constructor(
private val repository: MemberAreaRepository,
private val loginRepository: LoginRepository,
private val connectivityMonitor: ConnectivityMonitor,
) : ViewModel() {
private val _state = MutableStateFlow(QttrUiState())
val state: StateFlow<QttrUiState> = _state
init {
load()
viewModelScope.launch {
var wasOnline: Boolean? = null
connectivityMonitor.online.collect { online ->
if (online && wasOnline == false) {
load()
}
wasOnline = online
}
}
}
fun load() {

View File

@@ -3,6 +3,7 @@ package de.harheimertc.ui.screens.memberarea
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.harheimertc.data.ConnectivityMonitor
import de.harheimertc.data.BirthdayDto
import de.harheimertc.repositories.MemberAreaRepository
import kotlinx.coroutines.flow.MutableStateFlow
@@ -19,12 +20,22 @@ data class MemberAreaUiState(
@HiltViewModel
class MemberAreaViewModel @Inject constructor(
private val repository: MemberAreaRepository,
private val connectivityMonitor: ConnectivityMonitor,
) : ViewModel() {
private val _state = MutableStateFlow(MemberAreaUiState())
val state: StateFlow<MemberAreaUiState> = _state
init {
loadBirthdays()
viewModelScope.launch {
var wasOnline: Boolean? = null
connectivityMonitor.online.collect { online ->
if (online && wasOnline == false) {
loadBirthdays()
}
wasOnline = online
}
}
}
fun loadBirthdays() {

View File

@@ -8,8 +8,8 @@ LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/
PRODUCTION_API_BASE_URL=https://harheimertc.de/
# Android app versioning for Play Store uploads
ANDROID_VERSION_CODE=20
ANDROID_VERSION_NAME=0.9.15
ANDROID_VERSION_CODE=21
ANDROID_VERSION_NAME=0.9.16
# Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping.
RELEASE_MINIFY_ENABLED=false