Room persistence on Android
Room is Google's recommended SQLite persistence layer for Android. It is the concrete Android instantiation of the cookbook's transaction, normalization, and indexing guidance. Prefer coroutine-first DAO APIs: observable reads as Flow, one-shot writes as suspend, and multi-statement work wrapped in @Transaction.
Version landscape (as of 2026-06)
- Room 2.8.4 is the current stable line. Stable Kotlin Multiplatform (KMP) support arrived in 2.7.0 (Android + iOS + JVM desktop). State KMP support as "2.7.0+" — do not back-port the claim to older versions.
- Room 3.0.0-alpha01 (released 2026-03-11) is a major, breaking, KMP-first line that adds JS/WASM targets, generates only Kotlin, drops KAPT (KSP-only), and disallows blocking DAO functions. Room 3.0 is ALPHA — treat its API and the
androidx.room3:room3-*artifact rename as a FORECAST, not a shipped default. Do not migrate production code to it yet. - Pin the Room version explicitly in the version catalog; do not float to
+.
Compiler
- You MUST use KSP (
androidx.room:room-compilervia the KSP plugin) for the annotation processor. KAPT is legacy and roughly 2x slower; Room 3.0 removes KAPT support entirely. - You SHOULD enable schema export (
room.schemaLocation) and commit the generated JSON schema so migrations can be diffed and tested.
DAO async shape
- Observable reads SHOULD return
kotlinx.coroutines.flow.Flow<T>(orFlow<List<T>>). Room re-emits automatically whenever an underlying table changes — no manual invalidation. - One-shot reads and all writes (
@Insert,@Update,@Delete,@QueryDML) SHOULD besuspendfunctions so they run off the main thread on a Room-managed dispatcher. - You MUST NOT call blocking (non-
suspend, non-reactive) DAO functions on the main thread; Room throwsIllegalStateExceptionunlessallowMainThreadQueries()is set, which you SHOULD NOT use outside tests. - Reactive return types (
Flow, andFlowable/Observablevia the RxJava artifact) MUST NOT be markedsuspend.
Transactions
- Any operation that issues more than one statement and must be atomic (read-modify-write, multi-table insert, batch upsert with dependent rows) MUST be wrapped in a single transaction — annotate the DAO method with
@Transaction, or calldb.withTransaction { ... }from suspend code. @Transactionis also REQUIRED on@Querymethods that return a@Relation-bearing POJO, so the parent and child reads see a consistent snapshot.- Keep transactions short; do no network or long CPU work inside
withTransaction. Seeagenticdevelopercookbook://guidelines/implementing/data/transaction-isolationfor isolation semantics.
Schema and indexing
- Define one
@Entityper normalized table. Declare relationships with@ForeignKeyand load them via@Relation, not by denormalizing. - Every
@ForeignKeycolumn MUST be indexed (@Entity(indices = [Index("owner_id")])). Room emits a build warning for unindexed foreign keys; treat it as an error to fix. - Add an
@Index(unique = true)for natural-key uniqueness instead of relying on app-side checks.
Migrations
- On every schema change you MUST bump the database
versionand supply aMigration(or an@DeleteColumn/@RenameColumn-driven auto-migration spec). You MUST NOT shipfallbackToDestructiveMigration()in a release build — it silently drops user data. - You SHOULD add a
MigrationTestHelper-based instrumented test that opens the exported schema at version N and migrates to N+1, asserting data survives.
MUST NOT
- Do not hold the
RoomDatabaseinstance per-screen; build one application-scoped singleton viaRoom.databaseBuilder(...). - Do not expose
LiveDatafor new code when the consumer is a coroutine/Compose layer; preferFlow. - Do not perform multi-step writes as separate suspend calls without a transaction — a crash between calls leaves a partial, inconsistent state.