blog / libraries · architecture
Room vs DataStore vs SharedPreferences on Android
Room vs DataStore comes down to the shape of your data. Use DataStore for small settings and flags (theme, onboarding done, last selected tab), Room for anything that is a list of records you query, sort or relate (notes, messages, cached API results). For new code, SharedPreferences is the one to avoid; migrate it to DataStore with the built-in migration shown below.
Room vs DataStore vs SharedPreferences: which should I use?
| SharedPreferences | Preferences DataStore | Typed DataStore (JSON/Proto) | Room | |
|---|---|---|---|---|
| Data shape | Key-value | Key-value | One immutable object | Tables of rows |
| Good size | Tiny | Small (whole file is rewritten on each write) | Small | Thousands to millions of rows |
| Type safety | ✗ | Per key | ✓ | ✓ (SQL checked at compile time) |
| Async API | ✗ (reads can block the main thread) | ✓ Flow + suspend | ✓ Flow + suspend | ✓ Flow + suspend |
| Queries, partial updates | ✗ | ✗ | ✗ | ✓ |
| Transactions | ✗ | ✓ (whole store) | ✓ (whole store) | ✓ |
| Schema migrations | None | None needed | Handled by your serializer | Versioned migrations |
The rule of thumb: if you would ever write “find all items where…”, it belongs in Room. If you could fit it on a settings screen, it belongs in DataStore.
What is wrong with SharedPreferences?
SharedPreferences still works and is not deprecated, but it has design problems that cause real bugs:
- Synchronous reads on the main thread. The first
getString()waits for the whole XML file to load from disk. On a slow device this shows up as jank or even ANRs at startup. apply()is not as asynchronous as it looks. Pending writes are flushed when an activity or service stops, which can block the main thread at exactly the wrong moment.- No error signalling. A failed write or a parse error is silent.
- No transactional read-modify-write. Two coroutines incrementing the same counter can lose an update.
DataStore was built to fix exactly these: it reads and writes on a background dispatcher, exposes data as a Flow, surfaces I/O errors as exceptions, and runs every update as an atomic read-modify-write.
A side note on encryption: Jetpack Security's EncryptedSharedPreferences has been deprecated since security-crypto 1.1.0. For secrets, use the Android Keystore directly, and keep tokens out of plain preferences.
How do I use Preferences DataStore?
Add the dependency (1.3.0 is current as of November 2026; check the DataStore release page for the latest):
app/build.gradle.kts
dependencies {
implementation("androidx.datastore:datastore-preferences:1.3.0")
}
Create the DataStore once, at the top level of a Kotlin file, with the preferencesDataStore delegate. The delegate keeps it a singleton per process, which matters: two DataStore instances for the same file in one process throw IllegalStateException.
data/SettingsRepository.kt
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
data class UserSettings(val darkTheme: Boolean, val language: String)
class SettingsRepository(private val dataStore: DataStore<Preferences>) {
private object Keys {
val DARK_THEME = booleanPreferencesKey("dark_theme")
val LANGUAGE = stringPreferencesKey("language")
}
val settings: Flow<UserSettings> = dataStore.data.map { prefs ->
UserSettings(
darkTheme = prefs[Keys.DARK_THEME] ?: false,
language = prefs[Keys.LANGUAGE] ?: "en",
)
}
suspend fun setDarkTheme(enabled: Boolean) {
dataStore.edit { prefs -> prefs[Keys.DARK_THEME] = enabled }
}
}
Pass context.settingsDataStore into the repository from your DI setup, and expose settings to the UI through a ViewModel with stateIn. The Android app architecture guide covers that layer.
Two things trip people up:
- Reading once. If you need a single value outside a Flow collector, for example at startup, use
dataStore.data.first()from a coroutine. Do not wrap it inrunBlockingon the main thread; that recreates the SharedPreferences problem. - Handling I/O errors.
datathrowsIOExceptionif the file cannot be read. Catch it in the flow and emit defaults with.catch { if (it is IOException) emit(emptyPreferences()) else throw it }placed beforemap.
When should I use typed DataStore instead?
Preferences DataStore gives you type safety per key, not for the whole object. If your settings are a structured object, typed DataStore stores one immutable value through a Serializer you provide. The official docs show both Protocol Buffers and JSON via kotlinx.serialization; JSON is the easier start if you already use kotlinx.serialization. The type you store must be immutable, which a Kotlin data class with val properties satisfies; update it with dataStore.updateData { it.copy(...) }.
How do I migrate from SharedPreferences to DataStore?
DataStore ships a migration that copies values from an existing SharedPreferences file the first time the DataStore is accessed. Pass it through produceMigrations:
data/SettingsDataStore.kt
import androidx.datastore.preferences.SharedPreferencesMigration
// The old file was created with getSharedPreferences("app_prefs", MODE_PRIVATE)
private const val OLD_PREFS_NAME = "app_prefs"
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, OLD_PREFS_NAME))
},
)
How it behaves, from the API reference:
- Migrations run before DataStore returns any data, so the first read waits for the copy.
- Keys keep their names, so
booleanPreferencesKey("dark_theme")reads whatputBoolean("dark_theme", …)wrote. Make sure each key's type matches the type it was stored with. - Only boolean, float, int, long, string and string set values are migrated; anything else is ignored.
- To migrate only some keys, pass
keysToMigrate = setOf("dark_theme", "language"). Keys already present in DataStore are not overwritten. - When the SharedPreferences file is empty after migration, it is deleted.
If you used PreferenceManager.getDefaultSharedPreferences(), the file name is your package name followed by _preferences, for example com.example.app_preferences.
After migrating, remove every remaining read of the old SharedPreferences. Code that still writes to the old file after migration creates values that DataStore will never see.
Ship the migration in one release and delete the old SharedPreferences code in the same release. Running both side by side for “safety” is how settings end up split between two files.
How do I set up Room?
Room is an abstraction over SQLite with compile-time checked SQL, generated DAOs and migrations. As of 2026 there are two major lines:
- Room 3.x (
androidx.room3, stable since July 2026) is Kotlin- and coroutine-first. It requires KSP, generates only Kotlin, requires DAO functions to besuspendor return a reactive type such asFlow, and needs an explicit SQLite driver. - Room 2.x (
androidx.room, 2.8.x) is what most existing apps and tutorials use. It is still released and supports KAPT, Java codegen and blocking DAOs.
For a new Kotlin app, start on Room 3. For an existing Room 2 app, there is no urgency to move; the migration changes package names and replaces SupportSQLite APIs with driver APIs, which matters if you use raw queries, callbacks or manual migrations. The versions below are current as of November 2026; check the Room 3 release notes before copying them.
app/build.gradle.kts
plugins {
id("com.android.application")
id("com.google.devtools.ksp") // use the KSP version matching your Kotlin setup
id("androidx.room3") version "3.0.3"
}
room3 {
// Exported schemas: commit this folder; needed for auto-migrations
schemaDirectory("$projectDir/schemas")
}
dependencies {
implementation("androidx.room3:room3-runtime:3.0.3")
ksp("androidx.room3:room3-compiler:3.0.3")
}
In a project with a version catalog, put the Room version in libs.versions.toml once and reference it from the plugin and both libraries; see Gradle version catalogs.
An entity, a DAO and the database:
data/AppDatabase.kt
@Entity(tableName = "notes")
data class NoteEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val body: String,
@ColumnInfo(name = "updated_at") val updatedAt: Long,
val pinned: Boolean = false,
)
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY pinned DESC, updated_at DESC")
fun observeAll(): Flow<List<NoteEntity>>
@Query("SELECT * FROM notes WHERE title LIKE '%' || :query || '%'")
suspend fun search(query: String): List<NoteEntity>
@Upsert
suspend fun upsert(note: NoteEntity)
@Query("UPDATE notes SET pinned = :pinned WHERE id = :id")
suspend fun setPinned(id: Long, pinned: Boolean)
@Delete
suspend fun delete(note: NoteEntity)
}
@Database(entities = [NoteEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
}
Build the database once and share it, through a DI singleton or your Application. Each RoomDatabase instance is expensive.
di/DatabaseModule.kt
val db = Room.databaseBuilder<AppDatabase>(applicationContext, "app.db")
.setDriver(AndroidSQLiteDriver())
.build()
AndroidSQLiteDriver uses the SQLite built into Android and adds nothing to your app. The alternative, BundledSQLiteDriver from androidx.sqlite:sqlite-bundled, ships its own SQLite build: the same version on every device and required for Kotlin Multiplatform, at the cost of a native library in every APK. For Android-only apps the framework driver is the lighter default.
Notice what the DAO does that DataStore cannot: setPinned updates one column of one row without rewriting everything, search filters in SQL, and observeAll emits again whenever the notes table changes. That is the line between the two libraries.
How do Room schema changes work?
When you add a column or table, bump version in @Database. For simple changes, declare an auto-migration, which Room generates by comparing exported schemas: @Database(entities = [...], version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). Renames and deletes need an AutoMigrationSpec or a manual Migration. This is why the schema directory has to be committed: without the version 1 JSON, Room cannot generate the 1 → 2 migration.
Avoid fallbackToDestructiveMigration() in production unless the database is purely a cache you can refetch. It deletes user data on upgrade.
Can I use Room and DataStore in the same app?
Yes, and most apps should. A typical split:
- DataStore: theme, language, notification toggles, onboarding completed, last sync timestamp, feature flags.
- Room: user content, offline caches of API responses, drafts, search history, anything paged in a list.
- Neither: large binary files (images, audio, exports) go in
filesDirorcacheDir; store only the path in Room.
Warning signs that you picked wrong: a DataStore key holding a JSON array that grows over time (move it to Room), or a Room table with exactly one row of settings (move it to DataStore). Both libraries are in the essential Android libraries list for this reason; they complement each other rather than compete.
Decide by access pattern, not by size alone. If you ever need to change or query part of the data without loading the rest, use Room.
Both DataStore and Room files live in your app's private storage and are included in Auto Backup by default. If either contains tokens or device-specific state, exclude those files in data_extraction_rules.xml.
Frequently asked questions
Is SharedPreferences deprecated?
No, SharedPreferences itself is not deprecated and existing code keeps working. Google recommends DataStore for new code because SharedPreferences can block the main thread and has no error handling or transactional updates. EncryptedSharedPreferences from Jetpack Security is deprecated.
Can DataStore store a list of objects?
Technically yes, with typed DataStore and a serializer, but every write rewrites the whole file and you cannot query or update one item. If the list grows or you need to edit individual entries, use Room.
Is DataStore faster than SharedPreferences?
Raw read speed is similar for small data. The difference is that DataStore never blocks the main thread, while SharedPreferences can block on the first read and on pending apply() writes, which is what causes jank and ANRs.
Should I use Room 2 or Room 3 for a new app?
For a new Kotlin app, Room 3 is the forward-looking choice: it is stable, coroutine-first and requires KSP. Stay on Room 2.x if you depend on KAPT, Java code generation or blocking DAO calls, or if migrating SupportSQLite usages is not worth it yet.
Does SharedPreferencesMigration delete my old preferences?
It deletes the SharedPreferences file only when it is empty after migration, which happens when all keys were migrated. If you migrate only some keys with keysToMigrate, the remaining keys stay in the old file.