Room & local storage: DataStore vs SharedPreferences
Room is a compile-time-validated ORM layer over SQLite. Three building blocks:
- @Entity — a table (a data class)
- @Dao — the query interface:
@Querystrings are validated against SQL at compile time - @Database — the DB holder, owner of the version and migrations
Strengths: queries returning Flow<List<T>> — automatic re-emission when the table changes (the foundation of reactive UI); suspend DAO methods; atomic operations via @Transaction; the migration mechanism.
For small data:
- SharedPreferences — legacy key-value: synchronous API (commit() blocks on disk), no type safety, weak multi-process support.
- DataStore — the modern replacement: Flow-based, suspend API (never blocks the main thread), in Preferences and Proto (typed schema) flavors. Official guidance: new code should use DataStore.
Migrations are an interview classic: bump the version, write a Migration(1, 2) with SQL; omit it → a crash, or data loss with fallbackToDestructiveMigration.
@Entity(tableName = "transactions")
data class TransactionEntity(
@PrimaryKey val id: String,
val accountId: String,
val amountMinor: Long,
val createdAt: Long
)
@Dao
interface TransactionDao {
@Query("SELECT * FROM transactions WHERE accountId = :accountId ORDER BY createdAt DESC")
fun observeByAccount(accountId: String): Flow<List<TransactionEntity>>
@Upsert
suspend fun upsertAll(items: List<TransactionEntity>)
}
// v1 → v2: yeni sütun
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE transactions ADD COLUMN category TEXT")
}
}
Room.databaseBuilder(context, BankDb::class.java, "bank.db")
.addMigrations(MIGRATION_1_2)
.build()Room's core pieces and a migration
Banking local-storage rule: sensitive data (tokens, PIN hashes) must never sit in plaintext in SharedPreferences or plain DataStore — use Keystore-backed encryption or EncryptedSharedPreferences (detailed in the security stage). In the Room DB, apply data minimization: the full card number is never stored locally at all, only the masked form.
🛠 Practice task
Build a transactions table with Room: @Entity, @Dao (with a Flow-returning query), @Database.
- Subscribe to the Flow (in the UI or a test), insert a row, and watch the list update without calling anything manually.
- Add a column, bump the version, run it without a migration and see the crash; then fix it with
Migration(1,2). - Store one preference in DataStore and read it back as a Flow.
Done when: you can describe what happens without a migration and why fallbackToDestructiveMigration is dangerous.
📚 Sources and documentation
- Save data with Roomofficialdeveloper.android.com
- DataStoreofficialdeveloper.android.com
- Migrating Room databasesofficialdeveloper.android.com