Sparround

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: @Query strings 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.

kotlin
@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