An offline-first app works fully without a network connection and syncs when connectivity returns. This approach dramatically improves perceived performance, reliability on poor connections, and user trust. Room and DataStore are the primary tools Android provides for local persistence.
Why Offline-First Matters
Users expect apps to open instantly and show meaningful content, not a loading spinner. Studies show that apps with reliable offline support have significantly better retention rates. For Android specifically, connectivity is unpredictable — users move between Wi-Fi, mobile data, and dead zones constantly.
Room as the Source of Truth
The core principle: your UI should only read from Room, never directly from the network. Network responses write into Room, Room notifies the UI via Flow.
// Repository pattern
class NewsRepository(
private val dao: NewsDao,
private val api: NewsApi
) {
val news: Flow<List<Article>> = dao.getAll()
suspend fun refresh() {
val remote = api.fetchLatest()
dao.upsertAll(remote.map { it.toEntity() })
}
}
Key Room features to use: @Upsert (Room 2.5+) for conflict-free inserts,
Flow-returning DAOs for reactive UI, and @Transaction for atomicity.
DataStore for Preferences and Settings
DataStore replaces SharedPreferences for storing key-value data and typed objects. It is built on Kotlin coroutines and Flow, making it safe to read and write on any coroutine context.
- Preferences DataStore — type-safe key-value storage. No schema required.
- Proto DataStore — strongly typed objects via Protocol Buffers. Better for complex settings.
val Context.settingsDataStore by preferencesDataStore("settings")
// Write
suspend fun setDarkMode(enabled: Boolean) {
context.settingsDataStore.edit { prefs ->
prefs[DARK_MODE_KEY] = enabled
}
}
// Read
val darkMode: Flow<Boolean> = context.settingsDataStore.data
.map { prefs -> prefs[DARK_MODE_KEY] ?: false }
Sync Strategy
Three common sync patterns and when to use each:
- On-demand sync — triggered by user action (pull-to-refresh). Simple and predictable.
- Periodic sync — WorkManager with a periodic work request. Good for feeds and notifications.
- Push sync — Firebase Cloud Messaging triggers a background sync. Best for real-time data.
Handling Conflicts
When local changes exist alongside remote changes, you need a conflict resolution policy.
The simplest: last-write-wins with a lastModified timestamp on every entity.
For more complex scenarios, consider event sourcing or a CRDT approach.
Testing Offline Behavior
- Use Android Emulator network throttling to simulate slow/no connectivity.
- Use a
FakeNewsApiin tests that throwsIOExceptionto verify your app degrades gracefully. - Test that the app shows cached data immediately and refreshes when connectivity returns.