Local Persistence with Room
Room is Android's official persistence library — a type-safe, compile-time-checked layer over SQLite that turns annotated Kotlin classes into a real relational database without the boilerplate.
Learn Local Persistence with Room in our free Kotlin course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
By the end you'll define an @Entity , write a reactive @Dao , and wire up a RoomDatabase .
What You'll Learn in This Lesson
1️⃣ Defining a Table with @Entity
An @Entity turns a data class into a table: each instance is a row, each property a column. Mark one property with @PrimaryKey , optionally with autoGenerate = true so Room assigns IDs.
Room reads these annotations at compile time and generates the matching SQLite schema for you.
2️⃣ Querying with a @Dao
A @Dao interface declares your data access. Use @Query for custom SQL, plus @Insert / @Update / @Delete . Return a Flow for reactive reads, and mark writes suspend .
Because observeAll() returns a Flow , your UI refreshes itself whenever the data changes — no polling required.
3️⃣ The @Database Class
The @Database class extends RoomDatabase , lists every entity and a version, and exposes abstract DAO getters. Build a single instance with Room.databaseBuilder .
Mini-Challenge: A Task Store
Model a Task entity, a reactive DAO, and a database that exposes it.
Common Errors (and the fix)
- "Cannot access database on the main thread" — Mark write methods suspend and call them from a coroutine, or use a Flow read.
- "A migration from N to N+1 was required but not found" — When you change the schema, bump the version AND add a Migration .
- "Cannot figure out how to save this field" — Add a @TypeConverter for unsupported types like Date .
- Creating a new database every call — databaseBuilder is expensive; build one instance and reuse it.
Pro Tips
- 💡 Flow everywhere for reads: reactive queries keep the UI in sync with zero manual refreshing.
- 💡 Model relationships with @Relation and @Embedded instead of duplicating data.
- 💡 Test your migrations with the Room testing helpers so upgrades never lose user data.
📋 Quick Reference — Room
Concept
Room API
Define a table
@Entity data class User(...)
Primary key
@PrimaryKey(autoGenerate = true)
Custom query
@Query("SELECT * FROM users")
Reactive read
fun all(): Flow<List<User>>
Safe write
suspend fun insert(u: User)
The database
@Database(...) : RoomDatabase()
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ Room is a type-safe layer over SQLite
- ✅ @Entity defines tables; @PrimaryKey identifies rows
- ✅ A @Dao declares queries with @Query / @Insert / @Update / @Delete
- ✅ Flow reads are reactive; writes are suspend
- ✅ Migrations and type converters keep the schema flexible
- ✅ Next lesson: Dependency Injection with Hilt
Practice quiz
What annotation marks a class as a Room database table?
- @Entity
- @Model
- @Persist
- @Table
Answer: @Entity. @Entity marks a data class as a table; each instance is a row and each property a column.
Which library does Room sit on top of?
- Realm
- A custom NoSQL engine
- SQLite
- Firebase
Answer: SQLite. Room is an abstraction layer over Android's built-in SQLite, removing boilerplate while keeping full SQL access.
What annotation marks an interface that defines database access methods?
- @Repository
- @Dao
- @Access
- @Query
Answer: @Dao. A Data Access Object (@Dao) declares the methods used to read and write data.
Which annotation lets you write a custom SELECT statement in a DAO?
- @Sql
- @Select
- @Fetch
- @Query
Answer: @Query. @Query holds an arbitrary SQL statement; Room validates it at compile time.
How do you define the primary key for an entity?
- With @PrimaryKey on a property
- By naming a property 'id'
- Room picks one automatically with no annotation
- With the @Key annotation on the class
Answer: With @PrimaryKey on a property. @PrimaryKey marks the property; add autoGenerate = true to let Room assign incrementing IDs.
Why return a Flow from a Room @Query?
- It encrypts the rows
- It makes the query observable, re-emitting when the underlying data changes
- It runs the query on the main thread
- It compresses the result
Answer: It makes the query observable, re-emitting when the underlying data changes. A Flow-returning query is reactive: Room re-emits a fresh result whenever the table's data changes.
Why should insert/update/delete DAO methods usually be 'suspend' functions?
- Because Room requires it for compilation
- To make them faster than SQL
- So they return a Flow automatically
- So they run off the main thread without blocking the UI
Answer: So they run off the main thread without blocking the UI. Marking write methods suspend lets you call them from coroutines so disk I/O never blocks the UI thread.
What class must your database class extend, annotated with @Database?
- DatabaseHelper
- ContentProvider
- RoomDatabase
- SQLiteOpenHelper
Answer: RoomDatabase. The @Database-annotated abstract class extends RoomDatabase and exposes abstract DAO getters.
What is a Room migration used for?
- Moving the app to a new device
- Defining how to upgrade the schema from one version to the next
- Copying data to the cloud
- Switching from SQLite to MySQL
Answer: Defining how to upgrade the schema from one version to the next. A Migration tells Room the SQL needed to evolve the schema when the database version increases, preserving user data.
What does a @TypeConverter let Room do?
- Store a custom type (like Date) by converting it to and from a type SQLite supports
- Convert Kotlin to Java
- Encrypt a column
- Convert between SQL dialects
Answer: Store a custom type (like Date) by converting it to and from a type SQLite supports. A @TypeConverter maps an unsupported type (such as Date or an enum) to a primitive SQLite can store, and back.
Continue this course
- Previous: Kotlin Multiplatform Intro
- Next: Dependency Injection with Hilt