feat(sync): add periodic sync and pull-to-refresh sync in Stats screen

- Pull-to-refresh now triggers a background sync to fetch new data
- Automatic sync every 60 seconds while Stats screen is visible
- Uses ExistingWorkPolicy.KEEP to avoid interrupting running syncs
- Skips sync if already syncing (via SyncManager.syncStatus check)

This ensures new drives/charges are synced while the app is running,
without requiring the user to close and reopen the app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Davide Ferrari
2026-01-06 20:57:42 +01:00
parent 5f65c84282
commit 7eae6522cf
3 changed files with 53 additions and 3 deletions

View File

@@ -7,8 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- **Stats Sync**: Initial sync now starts automatically on first-time setup instead of requiring manual "Force Full Resync"
### Added
- **Stats Sync**: Pull-to-refresh in Stats screen now triggers a background sync
- **Stats Sync**: Automatic sync every 60 seconds while Stats screen is visible
## [0.8.0] - 2026-01-05

View File

@@ -103,6 +103,14 @@ fun StatsScreen(
viewModel.setCarId(carId)
}
// Periodic sync every 60 seconds while the screen is visible
LaunchedEffect(Unit) {
while (true) {
kotlinx.coroutines.delay(60_000L) // Wait 60 seconds
viewModel.triggerSync()
}
}
LaunchedEffect(uiState.error) {
uiState.error?.let { error ->
snackbarHostState.showSnackbar(error)

View File

@@ -1,8 +1,16 @@
package com.matedroid.ui.screens.stats
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import com.matedroid.data.repository.StatsRepository
import com.matedroid.data.sync.DataSyncWorker
import com.matedroid.data.sync.SyncLogCollector
import com.matedroid.data.sync.SyncManager
import com.matedroid.domain.model.CarStats
@@ -10,8 +18,8 @@ import com.matedroid.domain.model.SyncPhase
import com.matedroid.domain.model.SyncProgress
import com.matedroid.domain.model.YearFilter
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -33,6 +41,7 @@ data class StatsUiState(
@HiltViewModel
class StatsViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val statsRepository: StatsRepository,
private val syncManager: SyncManager,
private val syncLogCollector: SyncLogCollector
@@ -44,6 +53,9 @@ class StatsViewModel @Inject constructor(
/** Sync logs for debug viewing */
val syncLogs: StateFlow<List<String>> = syncLogCollector.logs
/** Expose sync status for UI to observe */
val syncStatus = syncManager.syncStatus
private var carId: Int? = null
private var syncObserverJob: Job? = null
@@ -88,11 +100,40 @@ class StatsViewModel @Inject constructor(
fun refresh() {
viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
// Trigger sync to fetch new data from server
triggerSync()
loadStatsInternal()
_uiState.update { it.copy(isRefreshing = false) }
}
}
/**
* Trigger a background sync to fetch new data from the server.
* Uses KEEP policy to avoid interrupting a running sync.
*/
fun triggerSync() {
// Skip if sync is already running
if (syncManager.syncStatus.value.isAnySyncing) {
return
}
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val syncRequest = OneTimeWorkRequestBuilder<DataSyncWorker>()
.setConstraints(constraints)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.addTag(DataSyncWorker.TAG)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
DataSyncWorker.WORK_NAME,
ExistingWorkPolicy.KEEP, // Don't interrupt running sync
syncRequest
)
}
fun clearError() {
_uiState.update { it.copy(error = null) }
}