:sync
This module handles background data synchronization using WorkManager. It ensures data consistency between local and remote data sources by performing periodic and on-demand sync operations.
Features
Background Synchronization
Periodic Sync Scheduling
Work Constraints Management
Progress Tracking
Error Handling
Hilt Worker Integration
Dependencies Graph
Usage
dependencies {
implementation(project(":sync"))
}Setting Up Sync
Make your repository syncable:
interface YourRepository : Syncable {
override suspend fun sync(): Flow<SyncProgress>
}Content copied to clipboardCreate sync worker:
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val repository: YourRepository
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
repository.sync()
.collect { progress ->
setProgress(progress.toWorkData())
}
return Result.success()
}
}Content copied to clipboardRequest sync operation:
class YourRepositoryImpl @Inject constructor(
private val syncManager: SyncManager
) : YourRepository {
fun requestSync() {
syncManager.requestSync()
}
}Content copied to clipboard
Work Constraints
The sync operation respects the following constraints:
Network availability
Battery not low
Storage not low
Device idle (for periodic sync)
Progress Tracking
data class SyncProgress(
val total: Int = 0,
val current: Int = 0,
val message: String? = null
)The sync progress can be observed from the WorkManager's progress updates.
Troubleshooting
This section covers common sync issues and their solutions. For general troubleshooting, see ../docs/troubleshooting.md.
Sync Not Running
Symptom: isSyncing never emits true, pull-to-refresh doesn't trigger sync
Common Causes:
Network constraint not met
// Check logcat for:
// "Work [...] not run because constraints not met"Content copied to clipboardSolution: Ensure device has active network connection. The sync requires
NetworkType.CONNECTED.WorkManager not initialized
Solution: Verify
Sync.initialize(context)is called inApp.onCreate():// In app/src/main/kotlin/dev/atick/jetpack/App.kt
override fun onCreate() {
super.onCreate()
Sync.initialize(this) // Must be called here
}Content copied to clipboardSync already running (duplicate request ignored)
// Check logcat for:
// "Requesting sync"
// WorkManager will KEEP existing work if already enqueuedContent copied to clipboardSolution: This is expected behavior. Wait for current sync to complete.
Sync Failing Repeatedly
Symptom: Sync fails after 3 retry attempts, error in logcat
Common Causes:
Network errors during sync
// Check logcat for:
// "Error syncing, retrying (1/3)" or "Error syncing" with stack traceContent copied to clipboardSolution: Check repository's
sync()implementation for proper error handling:override suspend fun sync(): Flow<SyncProgress> = flow {
try {
// Pull from remote
val remoteData = networkDataSource.getData()
localDataSource.save(remoteData)
// Push to remote
val localChanges = localDataSource.getPendingChanges()
networkDataSource.sync(localChanges)
emit(SyncProgress(total = 100, current = 100))
} catch (e: Exception) {
// Don't swallow exceptions - let SyncWorker handle retries
throw e
}
}Content copied to clipboardFirestore permission denied
Solution: Check Firestore security rules. See ../docs/firebase.md#firestore-security-rules.
Repository not implementing
SyncableSolution: Ensure repository extends
Syncableinterface:interface YourRepository : Syncable {
override suspend fun sync(): Flow<SyncProgress>
}Content copied to clipboard
Sync Notification Not Showing
Symptom: Sync runs but no foreground notification appears (Android 12+)
Common Causes:
Notification permission not granted (Android 13+)
Solution: Request notification permission in your app:
// In your activity/screen
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted -> /* handle result */ }
LaunchedEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}Content copied to clipboardForeground service type not declared (Android 14+)
Solution: Add foreground service declaration to
AndroidManifest.xml:<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />Content copied to clipboard
WorkManager Constraints Not Respected
Symptom: Sync runs even when battery is low or device is not idle
Solution: Customize SyncConstraints in SyncWorker.kt:
val SyncConstraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only
.setRequiresBatteryNotLow(true) // Battery not low
.setRequiresStorageNotLow(true) // Storage not low
.setRequiresDeviceIdle(true) // Device idle (API 23+)
.build()!IMPORTANT Adding strict constraints may delay sync significantly.
UNMETEREDnetwork requirement means sync will only run on WiFi.
Sync Conflict Resolution
Symptom: Local changes are overwritten by remote data during sync
Solution: Implement conflict resolution strategy in repository:
override suspend fun sync(): Flow<SyncProgress> = flow {
// 1. Pull remote changes
val remoteItems = networkDataSource.getItems()
// 2. Get local changes (not yet synced)
val localChanges = localDataSource.getPendingChanges()
// 3. Resolve conflicts (choose strategy)
val resolvedItems = remoteItems.map { remoteItem ->
val localChange = localChanges.find { it.id == remoteItem.id }
when {
localChange == null -> remoteItem // No conflict
localChange.updatedAt > remoteItem.updatedAt -> localChange // Local wins
else -> remoteItem // Remote wins
}
}
// 4. Save resolved data locally
localDataSource.saveAll(resolvedItems)
// 5. Push local-only items (new items created offline)
val localOnlyItems = localChanges.filter { local ->
remoteItems.none { it.id == local.id }
}
networkDataSource.createItems(localOnlyItems)
emit(SyncProgress(total = 1, current = 1))
}!TIP Common conflict resolution strategies:
Last Write Wins: Compare
updatedAttimestampsServer Wins: Always prefer remote data (simplest)
Client Wins: Always prefer local data (for offline-first apps)
Manual Resolution: Prompt user to choose (complex, better UX)
Debugging Sync Issues
Use Timber logging to trace sync execution:
// In your repository's sync() method
override suspend fun sync(): Flow<SyncProgress> = flow {
Timber.d("Sync started")
emit(SyncProgress(total = 100, current = 0, message = "Pulling remote data..."))
try {
val remoteData = networkDataSource.getData()
Timber.d("Pulled ${remoteData.size} items from remote")
emit(SyncProgress(total = 100, current = 50, message = "Saving locally..."))
localDataSource.save(remoteData)
Timber.d("Saved to local database")
emit(SyncProgress(total = 100, current = 100, message = "Sync complete"))
} catch (e: Exception) {
Timber.e(e, "Sync failed")
throw e // Let SyncWorker handle retries
}
}Check WorkManager state via ADB:
# Dump all WorkManager jobs
adb shell dumpsys jobscheduler | grep -A 20 "androidx.work"
# View WorkManager database
adb exec-out run-as dev.atick.jetpack sqlite3 /data/data/dev.atick.jetpack/databases/androidx.work.workdb "SELECT * FROM WorkSpec;"Related Documentation
../docs/troubleshooting.md - General troubleshooting patterns
../docs/firebase.md - Firestore setup and security rules
../data/README.md - Repository implementation patterns
WorkManager Documentation - Official Android WorkManager guide
Implementation Reference
SyncWorker:
sync/src/main/kotlin/dev/atick/sync/worker/SyncWorker.ktSyncManager:
sync/src/main/kotlin/dev/atick/sync/SyncManager.kt