What you'll learn
For structured data you need a real database, and on Android that's Room — a friendly layer over SQLite that turns tables into Kotlin classes and queries into checked functions. It works hand-in-glove with everything from Phase 4: suspend for writes, Flow for reactive reads.
By the end you'll be able to:
- Explain what Room is and when to reach for it
- Define an Entity (a table)
- Write a DAO with queries and mutations
- Build the database and observe data with
Flow
What Room is
Room is Jetpack's database library. Under the hood it's SQLite, but you never write raw boilerplate — you describe your data with annotations and Room generates the code. Best of all, it verifies your SQL at compile time: a typo in a query is a build error, not a crash in production. Room has three parts:
| Part | Annotation | Role |
|---|---|---|
Entity | @Entity | A class that maps to a table row |
DAO | @Dao | An interface of queries and write operations |
Database | @Database | Ties entities and DAOs together; the entry point |
Entities
An entity is a data class annotated with @Entity; each property becomes a column and each instance a row. One column is the @PrimaryKey — a unique id, usually auto-generated:
@Entity(tableName = "tasks")
data class Task(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val isDone: Boolean = false,
)DAOs & queries
A DAO (Data Access Object) is an interface declaring how you read and write. Convenience annotations — @Insert, @Update, @Delete — cover the basics, and @Query lets you write any SQL. Writes are suspend (they touch disk); reads that should stay live return a Flow:
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks ORDER BY id DESC")
fun observeAll(): Flow<List<Task>> // reactive reads
@Insert
suspend fun insert(task: Task) // writes are suspend
@Update
suspend fun update(task: Task)
@Delete
suspend fun delete(task: Task)
}Key idea
@Insert, @Query, @Update, and @Delete.The database
The @Database class lists your entities and exposes each DAO. You build one instance — and exactly one, as a singleton, since creating it is costly:
@Database(entities = [Task::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
// Build it once (a singleton — creating it repeatedly is expensive):
val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
val dao = db.taskDao()Observing with Flow
Here's where it all comes together. Because observeAll() returns a Flow, Room re-emits the full list every time the table changes — insert a task and the UI updates itself, no manual refresh. Expose it as StateFlow and drive writes through suspend functions:
class TaskViewModel(private val dao: TaskDao) : ViewModel() {
val tasks: StateFlow<List<Task>> = dao.observeAll()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
fun add(title: String) = viewModelScope.launch { dao.insert(Task(title = title)) }
fun toggle(task: Task) = viewModelScope.launch { dao.update(task.copy(isDone = !task.isDone)) }
}Tip
Recap & quick check
Key takeaways
- Room is Jetpack's database — a typed, compile-checked layer over SQLite for structured data.
- An @Entity data class maps to a table; @PrimaryKey identifies each row.
- A @Dao interface declares reads and writes: @Insert/@Update/@Delete and @Query for custom SQL.
- Writes are suspend functions; live reads return a Flow that re-emits when the table changes.
- Build the @Database as a singleton; use Migrations to preserve data across schema versions.
Quick check
1. What is Room?
2. What does @Entity annotate?
3. Why do live read queries return Flow<List<T>>?
4. Insert, update, and delete DAO functions are marked…
5. How should the @Database instance be created?
Your data persists locally. Now let's bring in data from the internet with Retrofit.