Offline-first architecture keeps users productive even without internet.
Room as Single Source of Truth
@Entity
data class Task(
@PrimaryKey val id: String,
val title: String,
val completed: Boolean = false,
val pendingSync: Boolean = true, // Track unsync'd changes
val lastModified: Long = System.currentTimeMillis()
)
@Dao
interface TaskDao {
@Query("SELECT * FROM Task WHERE pendingSync = 1")
fun getPendingSyncTasks(): Flow<List<Task>>
@Upsert
suspend fun upsertTask(task: Task)
}
Background Sync with WorkManager
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val pendingTasks = taskDao.getPendingSyncTasks().first()
for (task in pendingTasks) {
api.updateTask(task)
taskDao.update(task.copy(pendingSync = false))
}
Result.success()
} catch (e: Exception) {
Result.retry() // Retry with exponential backoff
}
}
}
// Schedule sync on network available
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"sync_tasks",
ExistingPeriodicWorkPolicy.KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.addTag("sync")
.build()
)
Repository Pattern
class TaskRepository(private val dao: TaskDao, private val api: TaskApi) {
fun getTasks(): Flow<List<Task>> = dao.getTasks()
suspend fun addTask(title: String) {
val task = Task(id = UUID.randomUUID().toString(), title = title)
dao.insert(task) // Insert immediately for offline
try {
api.createTask(task) // Sync when online
dao.update(task.copy(pendingSync = false))
} catch (e: Exception) {
// Remain in pendingSync=true for retry
}
}
}
Compose UI with PullToRefresh
@Composable
fun TaskListScreen(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsState()
var isRefreshing by remember { mutableStateOf(false) }
PullRefreshIndicator(
refreshing = isRefreshing,
onRefresh = {
isRefreshing = true
viewModel.syncNow { isRefreshing = false }
}
) {
LazyColumn {
items(tasks) { task ->
TaskItem(task)
}
}
}
}
Upsert for Safe API Updates
// If local version is newer, don't overwrite
suspend fun syncFromServer(remoteTasks: List<Task>) {
remoteTasks.forEach { remote ->
val local = dao.getTask(remote.id)
if (local?.lastModified ?: 0 < remote.lastModified) {
dao.upsert(remote.copy(pendingSync = false))
}
}
}
This pattern ensures zero data loss and seamless experience across network transitions.
8 production-ready Android app templates on Gumroad.
Browse templates → Gumroad
Top comments (0)