Android path · The screen and its data · Topic 7 of 12

Local data with Room

Not started

Room is the Jetpack layer over SQLite, the database that ships with Android. You describe tables as Kotlin classes and queries as annotated functions, and at build time Room checks the SQL against the schema and generates the implementation. At run time your code calls suspend functions and collects Flows instead of handling cursors and threads by hand.

Why it matters: The phone is often offline and the process is often killed. A local database is the one store that survives both, and Room makes it hard to get the threading or the SQL wrong.

  1. 1Three parts

    Entities are tables, DAOs are the queries, and the @Database class ties them together and hands out DAOs.

  2. 2Checked at build time

    Room's annotation processor (KSP) validates your SQL against the schema and generates the code; a typo fails the build.

  3. 3Observe, don't poll

    A DAO method that returns Flow emits again whenever the tables it reads change.

  4. 4Migrations are part of the schema

    Bump the version, and give Room a migration (automatic or written) for every step users might still be on.

1

How it works

The explanation, with examples you can run.

The three parts

@Entity(tableName = "note")data class Note(    @PrimaryKey val id: String,    val title: String,    val body: String,    val updatedAt: Long,) @Daointerface NoteDao {    @Query("SELECT * FROM note ORDER BY updatedAt DESC")    fun observeAll(): Flow<List<Note>>     @Query("SELECT * FROM note WHERE id = :id")    suspend fun get(id: String): Note?     @Upsert    suspend fun save(note: Note)} @Database(entities = [Note::class], version = 1, exportSchema = true)abstract class AppDatabase : RoomDatabase() {    abstract fun notes(): NoteDao}

Build the database once and share it (a DI singleton):

val db = Room.databaseBuilder<AppDatabase>(context, "app.db").build()

Threads and observation

Room's suspend DAO functions are main-safe: they run on Room's own executor and resume you where you were. In Room 2.x, a blocking (non-suspend) query called on the main thread throws unless you explicitly allow it, which you shouldn't. A Flow return type turns a query into a live view: Room re-runs it whenever a write to a table it reads commits, so the screen updates itself after a save anywhere in the app.

Group writes that belong together in a transaction (@Transaction on a DAO method, or db.withTransaction { ... } in 2.x) so observers see all of them or none.

Migrations

The schema version is part of your app's contract with every installed copy. When you change an entity, bump version and tell Room how to get there:

@Database(    entities = [Note::class],    version = 2,    autoMigrations = [AutoMigration(from = 1, to = 2)],   // needs exported schema JSON for both versions)abstract class AppDatabase : RoomDatabase() { /* ... */ } // For changes Room can't infer (renames, splitting tables), write it:val MIGRATION_2_3 = object : Migration(2, 3) {    override fun migrate(db: SupportSQLiteDatabase) {        db.execSQL("ALTER TABLE note ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0")    }}

Keep the exported schema files in version control and test migrations with MigrationTestHelper. Without a migration path, opening the database fails; fallbackToDestructiveMigration() makes it wipe the data instead, which is only acceptable for pure caches.

Room 2.x and Room 3.0

Version note: most apps today use Room 2.x (package androidx.room). Room 3.0, released in July 2026, moves to a new package (androidx.room3), is built around Kotlin Multiplatform, generates Kotlin only, requires KSP (no KAPT), and makes database operations coroutine-based: DAO functions must be suspend unless they return a reactive type like Flow. The SupportSQLite APIs (used in the migration above) are replaced by SQLite driver APIs unless you add the compatibility wrapper. The entity, DAO and database annotations stay the same, so the model on this page carries over.

Room as the source of truth

A common offline-first shape: the UI observes Room; a repository fetches from the network and writes into Room; the Flow delivers the update. The screen works offline, survives process death, and has one place where data comes from.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A DAO method is declared fun observeAll(): Flow<List<Note>>. When does it emit?
  2. 2.You add a column and bump the database version from 3 to 4, but provide no migration. What happens on a phone that has version 3?
  3. 3.Why does Room recommend a single RoomDatabase instance per process?

0 of 3 right so far. Get all three to finish this topic.