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.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.