In many parts of the world, network connectivity is unreliable. Even in major cities, mobile users frequently lose signal while commuting, entering buildings, or during power outages.
If your app stops working the moment internet access is lost, you’re building for ideal conditions , not the real world.
Offline-First Design
An offline-first application is designed to work seamlessly with or without the internet. It uses local storage as the primary data source and synchronizes changes with a remote server once connectivity is available.
This approach ensures your users can:
Continue working uninterrupted
Avoid data loss
Trust your app to be available at all times
Real-World Use Case: Field Data Collection in Rural Areas
I will share my approach to building a field data collection app for areas with poor internet. I had to ensure that:
- User input was never lost
- Data could be submitted at any time — whether online or not
- The app could sync data automatically once network resumed.
Here’s how I implemented this using Room, WorkManager, and NetworkCallback in Android.
Persisting Data Locally with Room
Whenever a user captures data (e.g., survey responses or inspection records), it is saved in a local SQLite database using Room.
@Entity(tableName = "field_data")
data class FieldDataEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val formName: String,
val content: String,
val isSynced: Boolean = false,
val timestamp: Long = System.currentTimeMillis()
)
@Dao
interface FieldDataDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(fieldData: FieldDataEntity)
@Query("SELECT * FROM field_data WHERE isSynced = 0")
suspend fun getUnsyncedData(): List<FieldDataEntity>
@Query("UPDATE field_data SET isSynced = 1 WHERE id = :id")
suspend fun markAsSynced(id: String)
}
Syncing with the Server Using WorkManager:
Once the device regains network access, a background worker is triggered to sync all unsynced data in the room database.
class DataSyncWorker(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
private val dao = AppDatabase.getInstance(context).fieldDataDao()
private val api = ApiService.create()
override suspend fun doWork(): Result {
val unsynced = dao.getUnsyncedData()
for (item in unsynced) {
try {
val response = api.uploadFieldData(item)
if (response.isSuccessful) {
dao.markAsSynced(item.id)
sendInAppNotification("Data synced: ${item.formName}")
}
} catch (e: Exception) {
// Retry later
}
}
return Result.success()
}
private fun sendInAppNotification(message: String) {
// Optionally notify user
}
}
Detecting Network Availability
Used Connectivity Manager to detect when the device regains internet access and enqueue the sync worker.
fun registerNetworkCallback(context: Context) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val request = NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build()
connectivityManager.registerNetworkCallback(request, object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
enqueueSyncWorker(context)
}
})
}
fun enqueueSyncWorker(context: Context) {
val request = OneTimeWorkRequestBuilder<DataSyncWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
WorkManager.getInstance(context).enqueue(request)
}
Conflict Resolution Strategy
In my case, conflicts were minimal because users didn’t edit the same record from multiple devices. However, in more collaborative apps, you can handle conflicts by:
Last-write-wins: simplest, but risky
Merge strategies: combine changes from client + server
User-assisted: notify user to choose the correct version
User Feedback: In-App and Push Notifications
Once sync completes, we notify the user via:
In-app snackbar/toast (if app is foregrounded)
Push notification using a local notification
Key Lessons:
Prioritize local-first design when working in regions with poor internet
Always queue unsynced data instead of blocking the user
Use WorkManager + Room + NetworkCallback for resilient, testable sync logic.
Don’t forget about conflict resolution — design for edge cases
Have you built offline-first apps before? I’d love to hear how you approached syncing and caching in the comments.