100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Kotlin

Room Database Basics

Room is Jetpack's SQLite abstraction layer that provides compile-time query verification, entity mapping, and Flow-based observation for reactive Android persistence.

Data & NetworkingIntermediate10 min readJul 8, 2026
Analogies

Room Database Basics

Room sits on top of SQLite and removes most of the boilerplate that raw SQLite programming demands: manual cursor handling, hand-written SQL string concatenation, and no compile-time safety net for typos in column names. Room is built from three core pieces that work together — the @Entity classes that define your tables, the @Dao interfaces that define your queries, and a @Database class that ties entities and DAOs together into a single access point. Because Room validates every @Query annotation against your schema at compile time, a misspelled column name or a query that references a non-existent table fails the build instead of crashing at runtime. Room also integrates natively with Kotlin coroutines and Flow, so a DAO method can return a Flow<List<T>> that automatically re-emits whenever the underlying table changes, giving you reactive UI updates for free without wiring up ContentObservers by hand.

🏏

Cricket analogy: Room is like having a certified scorer instead of hand-tallying runs on scraps of paper; @Entity classes are the scorebook's columns, @Dao is the scorer's rulebook of allowed entries, and Flow<List<T>> auto-updates the live scoreboard whenever a run is added.

Entities, DAOs, and the Database class

An @Entity is a plain Kotlin data class annotated so Room can generate a matching table; each property becomes a column unless annotated with @Ignore, and exactly one property must be marked @PrimaryKey. A @Dao interface declares the operations you can perform against that table — @Insert, @Update, @Delete for mutations, and @Query for anything more specific, including joins and aggregate functions. The @Database abstract class lists every entity it manages, declares a schema version, and exposes abstract functions that return each DAO. You obtain an instance of this class through Room.databaseBuilder, typically wrapped in a singleton so the app never opens more than one connection to the same file.

🏏

Cricket analogy: An @Entity is like a scorecard template with exactly one designated column (the @PrimaryKey, like a unique match ID), while the @Dao is the scorer's permitted actions — record a run, correct an entry, query the innings total via joins.

Suspend functions and Flow in DAOs

Room supports two complementary styles of DAO method. One-shot operations — inserting a row, deleting a row, running a single lookup — should be marked suspend so Room dispatches them off the main thread automatically and the call site can simply await the result inside a coroutine. Observational queries, where the UI needs to stay in sync with table contents over time, should return Flow<T> instead; Room manages the underlying invalidation tracking so the Flow emits a fresh list every time a relevant INSERT, UPDATE, or DELETE commits. Never call a non-suspend, non-Flow Room query directly on the main thread — Room throws an IllegalStateException by default specifically to prevent accidental main-thread database access, a safeguard you should not disable in production code.

🏏

Cricket analogy: Recording a single ball's outcome is a quick suspend operation you can await without blocking the whole broadcast, while a live win-probability graph needs a Flow that re-emits with every ball, just as Room forbids main-thread queries to prevent a frozen scoreboard.

kotlin
@Entity(tableName = "tasks")
data class TaskEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val title: String,
    val isDone: Boolean = false,
    val createdAt: Long = System.currentTimeMillis()
)

@Dao
interface TaskDao {
    @Insert
    suspend fun insert(task: TaskEntity): Long

    @Update
    suspend fun update(task: TaskEntity)

    @Delete
    suspend fun delete(task: TaskEntity)

    @Query("SELECT * FROM tasks WHERE isDone = 0 ORDER BY createdAt DESC")
    fun observePendingTasks(): Flow<List<TaskEntity>>

    @Query("SELECT * FROM tasks WHERE id = :taskId")
    suspend fun getTaskById(taskId: Long): TaskEntity?
}

@Database(entities = [TaskEntity::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao

    companion object {
        @Volatile private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app.db"
                ).build().also { INSTANCE = it }
            }
    }
}

Every schema change bumps the version integer on @Database, and Room requires you to supply a Migration object describing exactly how to transform the old schema into the new one via raw SQL, unless you explicitly opt into fallbackToDestructiveMigration, which simply wipes and recreates the database — acceptable during early development but destructive to real user data in production. Setting exportSchema = true writes a JSON snapshot of each version to disk, which the Room migration testing library can then use to verify a Migration actually produces the expected end schema before you ship it.

🏏

Cricket analogy: Changing a scoring rule mid-tournament (like adding a Super Over column) requires a documented Migration explaining exactly how old scorecards convert, unless you accept wiping all historical scores — fine in a practice match, unacceptable in a World Cup final.

A useful mental model: Room is to SQLite what a strongly typed ORM is to a database — you still get real SQL power (joins, indices, transactions) through @Query, but the compiler catches structural mistakes before your users ever do.

A common pitfall is instantiating Room.databaseBuilder more than once for the same file path, e.g. inside a Composable or a short-lived ViewModel. Each instance opens its own connection and can lead to database-locked errors or missed change notifications. Always obtain the database through a single, application-scoped instance, ideally injected via Hilt.

  • Room consists of @Entity tables, @Dao query interfaces, and an abstract @Database that wires them together.
  • @Query strings are validated against the schema at compile time, catching typos before runtime.
  • One-shot DAO operations should be suspend functions; continuously observed queries should return Flow<T>.
  • Room blocks main-thread database access by default — this safeguard should not be disabled.
  • Schema changes require an explicit Migration, or exportSchema/fallbackToDestructiveMigration for dev-time flexibility.
  • The database instance should be a single, application-scoped singleton, not created per screen or per ViewModel.

Practice what you learned

Was this page helpful?

Topics covered

#Kotlin#AndroidWithJetpackComposeStudyNotes#MobileDevelopment#RoomDatabaseBasics#Room#Database#Entities#DAOs#SQL#StudyNotes#SkillVeris