Android Room

Room is the default Android abstraction for non-trivial structured local data. It gives you entities, DAOs, compile-time SQL checks, migrations, and test helpers over SQLite.

@Entity(tableName = "transactions")
data class TransactionEntity(
    @PrimaryKey val id: String,
    val occurredAt: Instant,
    val amountCents: Long,
    val currency: String,
    val merchant: String?,
    val note: String?
)

@Dao
interface TransactionDao {
    @Query("SELECT * FROM transactions ORDER BY occurredAt DESC")
    fun observeAll(): Flow<List<TransactionEntity>>

    @Insert(onConflict = OnConflictStrategy.ABORT)
    suspend fun insert(transaction: TransactionEntity)
}

Room Pieces

PiecePurpose
Entitytable shape and indexes
DAOSQL boundary for reads and writes
Database classversion, entities, type converters, migrations
Migrationexplicit schema/data transition between versions
Schema exportJSON history used by migration tests and reviews

Migration Rule

For user-owned data, do not use destructive migration as a normal upgrade path.

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE transactions ADD COLUMN note TEXT")
    }
}

fallbackToDestructiveMigration() is useful in prototypes and disposable caches. It is dangerous for user-owned data because it can drop tables when Room cannot find a migration path.

Testing

Room migration tests should create an old database, insert real rows, migrate to the latest version, validate the schema, and assert the rows survived.

@Test
fun migrate1To2_preservesTransactions() {
    helper.createDatabase(TEST_DB, 1).apply {
        execSQL("INSERT INTO transactions (id, occurredAt, amountCents, currency) VALUES ('t1', '2026-01-01T10:00:00Z', 5000, 'KES')")
        close()
    }

    val db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)
    db.query("SELECT amountCents FROM transactions WHERE id = 't1'").use { cursor ->
        assertThat(cursor.moveToFirst()).isTrue()
        assertThat(cursor.getLong(0)).isEqualTo(5000L)
    }
}

Gotcha: Host-side SQLite may not match device SQLite. Use instrumented tests for migration behavior that must match real devices.