Phase 4 · Data & PersistenceModule 40~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,
)
Focused example: keep your generated package and theme. Add the shown APIs to your Compose project using Android Studio imports.

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)
}
Focused example: keep your generated package and theme. Add the shown APIs to your Compose project using Android Studio imports.

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 reuse it per database file in a process, 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()
Focused example: keep your generated package and theme. Add the shown APIs to your Compose project using Android Studio imports.

Observing with Flow

Here's where it all comes together. Because observeAll() returns a Flow, Room reruns an observable query when a referenced table is invalidated — 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)) }
}
Focused example: keep your generated package and theme. Add the shown APIs to your Compose project using Android Studio imports.

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).

Practice: one small working feature

Create a Room entity and DAO to insert and observe short reading notes.

Check your result

An inserted note appears from the DAO Flow and remains after an app restart.

Remember

Room is the source of saved records. Keep a draft separate; export schemas and add migrations when changing them.

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.