AppMama

blog / libraries · architecture

Room vs DataStore vs SharedPreferences on Android

· 8 min read · AppMama

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?

SharedPreferencesPreferences DataStoreTyped DataStore (JSON/Proto)Room
Data shapeKey-valueKey-valueOne immutable objectTables of rows
Good sizeTinySmall (whole file is rewritten on each write)SmallThousands 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 migrationsNoneNone neededHandled by your serializerVersioned 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:

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:

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:

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:

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:

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.