Phase 4 · Data & PersistenceModule 22~34 min read

Room Database

Store structured data locally with Room — entities, DAOs, queries, and observing with Flow.

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:

PartAnnotationRole
Entity@EntityA class that maps to a table row
DAO@DaoAn interface of queries and write operations
Database@DatabaseTies 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:

Task.kt
@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:

TaskDao.kt
@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

The four operations Create, Read, Update, Delete — "CRUD" — are the vocabulary of every database. Room maps them to @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:

AppDatabase.kt
@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:

TaskViewModel.kt
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

This is the reactive database loop: write → table changes → Flow re-emits → UI recomposes. You describe the query once, and the screen simply stays correct. When your schema changes in a new app version, you provide a Migration so existing users' data survives the upgrade (Room checks the version number).

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.