The Room Persistence Library
The Room Persistence Library
In the previous lesson you saw how to work with SQLite directly using SQLiteOpenHelper. That approach works, but it forces you to write repetitive boilerplate: create tables in raw SQL, write ContentValues packing and unpacking code, and cast columns by hand. Room is Google's official abstraction layer on top of SQLite. It eliminates most of that boilerplate while giving you compile-time verification of your SQL and seamless integration with the rest of the Jetpack ecosystem.
.db file on the device; Room just spares you from writing the repetitive parts by hand.
The Three Pillars of Room
Every Room setup involves exactly three types of class:
- Entity — a Java class annotated with
@Entity. Each instance maps to one row in a database table. - DAO (Data Access Object) — an interface or abstract class annotated with
@Dao. You declare methods here and Room generates the SQL implementation. - Database — an abstract class annotated with
@Databasethat extendsRoomDatabase. It is the main entry point that ties everything together.
Adding Room to Your Project
Open your module-level build.gradle and add the dependencies. Room ships as three separate artifacts:
annotationProcessor, not kapt. kapt is the Kotlin annotation processor. For a pure-Java Android project you must use annotationProcessor; otherwise Room's code generator will not run and you will get cryptic "cannot find symbol" errors at build time.
Defining an Entity
An entity is a plain Java object (POJO) decorated with Room annotations. Room reads these annotations at compile time and generates a CREATE TABLE statement for you.
Key annotation details:
@Entity(tableName = "tasks")— the generated table name. If you omit it, Room uses the class name lowercased.@PrimaryKey(autoGenerate = true)— tells Room to use SQLite'sAUTOINCREMENTequivalent. Every entity must have exactly one primary key.@ColumnInfo(name = "...")— maps the Java field to a specific column name. Optional, but strongly recommended so that renaming a Java field does not silently break your queries.
String, or types that have a registered @TypeConverter. If you try to store a List or a custom class directly, Room will refuse to compile with an error like "Cannot figure out how to save this field into database."
Defining the DAO
The DAO is where you declare the operations your app needs. Room generates the implementation. You write method signatures and SQL (or let Room infer the SQL for common CRUD operations).
Notice how the @Query annotation takes a plain SQL string. Room parses this SQL at compile time and reports errors — a mistyped column name or table name will cause a build failure, not a crash at runtime. This is one of Room's most valuable safety guarantees.
Creating the Database Class
The database class is the glue. It declares which entities belong to the schema, the schema version, and exposes factory methods to get DAO instances.
Two important decisions made here:
- Singleton pattern with double-checked locking — creating a
RoomDatabaseis expensive (it opens the file and compiles migrations). You want exactly one instance per process. context.getApplicationContext()— passing an Activity context would leak the Activity. The Application context lives for the full process lifetime and is safe to hold.
Using Room on a Background Thread
Room enforces a strict rule: database operations may not run on the main (UI) thread. Doing so would block the UI and cause ANR (Application Not Responding) errors. The simplest way to comply is to use a background thread explicitly:
LiveData<List<Task>> instead of plain List<Task>>. Room then runs the query on a background thread automatically and posts updates to observers on the main thread whenever the data changes. This is the recommended production pattern. For now, using explicit threads clearly shows what Room actually does under the hood.
Schema Migrations
When you add a column or table you must increment the version in @Database and provide a Migration object. Without a migration, Room will throw an IllegalStateException when it detects the schema version mismatch.
During development you can call .fallbackToDestructiveMigration() instead to drop and recreate the database on a version mismatch. Never use this in production — it permanently destroys the user's data.
Summary
Room reduces Android database work to three annotated classes: an @Entity that describes a table row, a @Dao interface where you declare queries (verified at compile time), and an @Database singleton that binds everything together. Always access Room on a background thread, use getApplicationContext() for the singleton, and define explicit Migration objects every time the schema changes. In the next lesson you will tackle background threading in depth and learn about the modern options — ExecutorService and WorkManager — that pair naturally with Room in production apps.