Reactive Sync Updates
Reactive Sync Updates
SQLiteNow’s reactive flows automatically update your UI when sync operations modify data. This page focuses on sync-specific patterns and behaviors.
📖 New to reactive flows? Start with the Reactive UI Updates recipe to learn the basics of using
.asFlow()
with your queries.
How Sync Triggers UI Updates
When sync operations modify your database tables, SQLiteNow’s table update listener automatically:
- Detects table changes during upload and download operations
- Notifies reactive flows that depend on those tables
- Triggers UI updates through your existing
.asFlow()
queries - Maintains consistency between local and remote data
This happens transparently - you don’t need to manually refresh your UI after sync operations.
Sync-Specific Reactive Patterns
Automatic UI Updates During Sync
Your existing reactive flows automatically update when sync operations modify data:
@Composable
fun SyncAwarePersonList() {
var persons by remember { mutableStateOf<List<PersonEntity>>(emptyList()) }
var isSyncing by remember { mutableStateOf(false) }
// Standard reactive query - no sync-specific code needed
LaunchedEffect(Unit) {
database.person.selectAll().asFlow()
.flowOn(Dispatchers.IO)
.collect { persons = it } // Updates during sync operations
}
// Sync function - UI updates happen automatically via table update listener
suspend fun performSync() {
isSyncing = true
try {
syncClient.uploadOnce() // May trigger flows if conflicts resolved
syncClient.downloadOnce() // Triggers flows when new data arrives
// No manual UI refresh needed!
} finally {
isSyncing = false
}
}
}
Table Update Listener Mechanism
SQLiteNow’s sync system uses a table update listener that:
- Monitors sync operations: Tracks which tables are modified during upload/download
- Notifies flow system: Calls
notifyTablesChanged()
with affected table names - Triggers reactive queries: Any
.asFlow()
queries watching those tables re-execute - Updates UI automatically: Compose recomposes with new data
This happens for both upload operations (when conflicts are resolved) and download operations (when remote changes are applied).
Sync Triggers with Reactive Updates
Event-Driven Sync
Combine manual sync triggers with automatic UI updates:
class SyncManager {
private val syncTrigger = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
init {
syncTrigger
.debounce(2000) // Wait 2 seconds after last trigger
.onEach { performSync() }
.launchIn(GlobalScope)
}
fun triggerSync() = syncTrigger.tryEmit(Unit)
private suspend fun performSync() {
try {
syncClient.uploadOnce()
syncClient.downloadOnce(limit = 1000)
// Table update listener automatically triggers reactive flows
} catch (e: Exception) {
logger.e(e) { "Sync failed" }
}
}
}
Pull-to-Refresh with Sync
@Composable
fun PersonListWithPullRefresh() {
var isRefreshing by remember { mutableStateOf(false) }
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
onRefresh = {
coroutineScope.launch {
isRefreshing = true
try {
syncClient.downloadOnce(limit = 1000)
// UI updates automatically via table update listener
} finally {
isRefreshing = false
}
}
}
)
// Standard reactive query - no sync-specific code needed
// Updates automatically when pull-to-refresh sync completes
}
Sync-Specific Considerations
Table Update Timing
The table update listener fires at specific points during sync:
- Upload operations: When conflicts are resolved and local data changes
- Download operations: When remote changes are applied to local tables
- Post-upload lookback: When consistency checks modify local data
Conflict Resolution Impact
When conflicts occur during upload, the resolver may modify local data:
// If resolver chooses AcceptServer, local data changes and flows update
// If resolver chooses KeepLocal, data stays the same, no flow update needed
Sync Performance
- Batch sync operations: Don’t sync after every single change
- Use appropriate page sizes: 1000 is usually optimal for downloads
- Monitor sync frequency: Balance freshness with battery/bandwidth usage
Best Practices for Sync + Reactive Flows
Sync Integration
- Let table update listener handle UI updates - don’t manually refresh after sync
- Use event-driven sync patterns for better user experience
- Implement pull-to-refresh for user-initiated sync operations
- Handle sync errors gracefully without breaking reactive flows
Performance
- Debounce sync triggers to avoid excessive sync operations
- Use specific queries in reactive flows to minimize update scope
- Monitor sync impact on UI performance during development
Error Handling
- Provide offline indicators when sync operations fail
- Implement retry logic for failed sync operations
- Keep reactive flows working even when sync is unavailable
Next Steps: Learn about Conflict Resolution to handle data conflicts between devices.