Intermediate: Android basics · Saving data and calling APIs · topic 34 of 50 · 30 min
Offline-first apps and DataStore
Explanation
Why offline-first?
Mobile networks are unreliable: lifts, metro tunnels, villages with weak signal, expensive data packs. An app that shows a blank screen whenever the network is slow feels broken. An offline-first app always shows something useful immediately (the last saved data) and updates it when the network allows.
The pattern
UI <-- observes -- Room (single source of truth) <-- saves -- Network
| ^
+--------------- "refresh" event --> Repository ----------------+class NewsRepository(private val api: NewsApi, private val dao: ArticleDao) {
val articles: Flow<List<Article>> = dao.observeAll() // the UI reads only this
suspend fun refresh(): Result<Unit> = try {
val fresh = api.latest().map { it.toEntity() }
dao.upsertAll(fresh) // the Flow emits the new list
Result.success(Unit)
} catch (e: IOException) {
Result.failure(e) // keep showing saved articles
}
}Room's @Upsert inserts new rows and updates existing ones in one call, which is handy for saving fresh network data. Because the screen observes the database Flow, it updates automatically after the save.
When should the app refresh? Common choices: when a screen opens, on pull-to-refresh, and periodically in the background. Many apps also skip the refresh if the data is still fresh, for example saved less than five minutes ago, to save the user's data pack and battery.
Preferences DataStore for small settings
Some data is not a table: a dark-mode switch, the chosen language, "has seen onboarding". For these, Android recommends Jetpack DataStore. Preferences DataStore stores key-value pairs, works with coroutines and Flow, and is safe to use from any thread. It replaces the older SharedPreferences.
val Context.settingsStore by preferencesDataStore(name = "settings")
private val DARK_MODE = booleanPreferencesKey("dark_mode")
class SettingsRepository(private val context: Context) {
val darkMode: Flow<Boolean> = context.settingsStore.data.map { it[DARK_MODE] ?: false }
suspend fun setDarkMode(on: Boolean) {
context.settingsStore.edit { it[DARK_MODE] = on }
}
}| Data | Where to store it |
|---|---|
| Lists of records: orders, notes, contacts | Room |
| Small settings and flags | Preferences DataStore |
| Login tokens and secrets | Encrypted storage (for example backed by the Android Keystore); never plain files |
| Big files: photos, PDFs | App-specific files directory |
Background sync with WorkManager
Some work must happen even if the user closes the app: uploading a form filled offline, syncing every few hours. WorkManager is the Jetpack library for deferrable, guaranteed background work. You describe a Worker, add constraints such as "only on Wi-Fi" or "only when charging", and Android runs it at a suitable time, even after a reboot.
Common mistakes
- Showing network data directly and the database separately: two sources of truth that disagree.
- Using SharedPreferences for new code: prefer DataStore.
- Creating several DataStore instances for the same file: errors and corruption. Use one per file.
- Running sync in a plain coroutine that dies with the app. Use WorkManager for guaranteed work.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Offline-first apps and DataStore · 1/6
Offline first means: show local data immediately, and refresh from the network in the background.
Prefer a video by a teacher? Find free videos on “android offline first repository room retrofit datastore workmanager” on YouTube ↗
In real life: A field survey app for rural health workers
Health workers in villages fill survey forms on phones with very weak signal. The app saves each form in Room immediately and marks it "pending". A WorkManager job with a "network connected" constraint uploads pending forms whenever signal is available and marks them "synced". The worker's preferred language is kept in DataStore. No form is ever lost because the network dropped.
@Entity
data class SurveyForm(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val village: String,
val householdSize: Int,
val synced: Boolean = false,
)
@Dao
interface SurveyDao {
@Query("SELECT * FROM SurveyForm WHERE synced = 0") suspend fun pending(): List<SurveyForm>
@Query("UPDATE SurveyForm SET synced = 1 WHERE id = :id") suspend fun markSynced(id: Long)
}Practice (0/3 done)
In an offline-first repository, what should the UI observe?
Where should a "dark mode on/off" setting be stored?
Turn your Retrofit list app from the previous topics into an offline-first app: add a Room entity and DAO for the items, a repository that exposes the DAO Flow and a refresh() that saves network results, and a settings screen with a DataStore switch for "show images". Test in airplane mode after one successful load.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Networking with RetrofitSaving data and calling APIs · same module
- Local storageFlutter and Dart · Networking, storage and Firebase
- Interview questions: Android and ComposeAndroid career path
- Saving data in the browserFrontend Web Development · JavaScript in the browser: DOM and events
- Portfolio and next stepsAndroid career path
- Ad-hoc commands and ansible-docLinux and Red Hat (RHCSA to RHCE) · Ansible fundamentals
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does offline-first mean?
Answer: The app shows local data first and syncs with the network when possible
Local data is always shown immediately.
Which library replaced SharedPreferences for new code?
Answer: DataStore
Jetpack DataStore is the recommended replacement.
Which tool guarantees background sync even after the app is closed or the phone restarts?
Answer: WorkManager
WorkManager persists and schedules work.
Where should a login token be stored?
Answer: Encrypted storage backed by the Keystore
Secrets need encryption.
Is shared_preferences encrypted?Related
Should a repository return Compose Color or UI objects?Related
Answer: No, the data layer should not know about UI
Keep layers independent.
What is the single source of truth in a typical offline-first list?Related
What database engine does Room use underneath?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Saving data and calling APIs”. The module exam comes next.
- Module 8 exam: Saving data and calling APIs · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Permissions, Material 3 and polish. 50 topics left in the course before the final exam.