Skip to content
intermediate Phase 5 · Jetpack Libraries

Room

Implement local database with Room: entities, DAOs, database, and migrations.

1h
4 problems
Topic Progress 0%

Entities and DAOs

Room Architecture

Room is SQLite wrapped in a compile-time verified abstraction. It has three main components:

  • Entity: A class that maps to a database table
  • DAO (Data Access Object): An interface defining database operations
  • Database: The holder for the database instance

Defining Entities

@Entity(tableName = "tasks")
data class Task(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val title: String,
    val description: String,
    val isCompleted: Boolean = false,
    @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)

@Entity marks the class as a database table. Each property maps to a column. Use @ColumnInfo to customize column names. @PrimaryKey(autoGenerate = true) creates an auto-incrementing ID.

Composite Primary Keys

@Entity(
    tableName = "task_tags",
    primaryKeys = ["taskId", "tagId"]
)
data class TaskTagCrossRef(
    val taskId: Long,
    val tagId: Long
)

Type Converters

Room only knows how to store primitives and Strings. For complex types, write a converter:

class DateConverter {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }

    @TypeConverter
    fun toTimestamp(date: Date?): Long? = date?.time
}

Register converters on the database class:

@Database(entities = [Task::class], version = 1)
@TypeConverters(DateConverter::class)
abstract class TaskDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

Writing DAOs

@Dao
interface TaskDao {
    @Query("SELECT * FROM tasks ORDER BY created_at DESC")
    fun getAllTasks(): Flow<List<Task>>

    @Query("SELECT * FROM tasks WHERE id = :taskId")
    fun getTaskById(taskId: Long): Flow<Task?>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTask(task: Task): Long

    @Update
    suspend fun updateTask(task: Task)

    @Delete
    suspend fun deleteTask(task: Task)

    @Query("DELETE FROM tasks WHERE isCompleted = 1")
    suspend fun deleteCompletedTasks()
}

Return Flow<List<Task>> for reactive queries — Room automatically re-emits when the table data changes. Use suspend for one-shot writes.

Database Setup and Migrations

Building the Database

Room enforces that you use its builder to create the database instance. In production, ensure only one instance exists:

@Database(entities = [Task::class], version = 1)
abstract class TaskDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao

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

        fun getInstance(context: Context): TaskDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    TaskDatabase::class.java,
                    "task_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}

Migrations

When you change the schema (add column, create table), you must provide a migration. Without one, Room throws an IllegalStateException and your app crashes.

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE tasks ADD COLUMN priority INTEGER NOT NULL DEFAULT 0")
    }
}

Register migrations when building the database:

Room.databaseBuilder(context, TaskDatabase::class.java, "task_database")
    .addMigrations(MIGRATION_1_2)
    .build()

Auto-Migrations (Room 2.4+)

For simple schema changes, Room can auto-generate migrations:

@Database(
    entities = [Task::class],
    version = 2,
    autoMigrations = [
        AutoMigration(from = 1, to = 2)
    ]
)

For complex migrations with data transformation, provide a spec:

@RenameColumn(tableName = "tasks", fromColumnName = "done", toColumnName = "isCompleted")
class Migration1To2 : AutoMigrationSpec

Testing Migrations

Room provides MigrationTestHelper to verify migrations in instrumented tests:

class MigrationTest {
    @get:Rule
    val helper = MigrationTestHelper(
        InstrumentationRegistry.getInstrumentation(),
        TaskDatabase::class.java
    )

    @Test
    fun migrate1To2() {
        helper.createDatabase("task_database", 1).apply {
            execSQL("INSERT INTO tasks (title, description) VALUES ('Test', 'Desc')")
            close()
        }

        helper.runMigrationsAndValidate("task_database", 2, MIGRATION_1_2).also {
            // Verify schema and data
        }
    }
}

Always test migrations with real data. A broken migration means data loss.

Quiz

1. What does Room do when you change the database version without providing a migration?

Question 1 options

2. Why should DAO query methods that return data use Flow instead of direct return values?

Question 2 options

3. When should you use @TypeConverters in Room?

Question 3 options

4. What is the purpose of the onConflict parameter in @Insert?

Question 4 options

Flashcards

Question

What are the three main components of Room?

Answer

Entity (maps to a table), DAO (defines database operations), and Database (holds the database instance and provides DAOs).

Question

What does @TypeConverters do in Room?

Answer

Tells Room how to convert complex types (Date, custom objects) to and from database-storable primitives using @TypeConverter functions.

Question

When do you need a Room migration?

Answer

Anytime you change the database schema after the initial creation — adding tables, columns, or modifying constraints. Room throws an error without one.

Question

What is the difference between @Insert, @Update, and @Delete in a DAO?

Answer

@Insert adds a new row (with onConflict strategy), @Update modifies an existing row by primary key, @Delete removes a row by primary key. All can be suspend functions.

Revision Notes

Key Takeaways

  • 1. Room enforces compile-time verification of SQL queries and database schema
  • 2. Use Flow in DAOs for reactive queries that automatically update when data changes
  • 3. Always provide a migration when changing the database version to avoid crashes
  • 4. TypeConverters are required for any type Room does not natively support
  • 5. Test migrations with real data to prevent data loss in production

Interview Tips

  • Explain Room's architecture: Entity, DAO, and Database components
  • Discuss how Room ensures compile-time query verification
  • Know how to write a manual migration and when to use AutoMigration
  • Explain why Flow is preferred over direct return values for queries

Cheat Sheet

Room Cheat Sheet

Entity:

  • @Entity(tableName = "table") — marks class as table
  • @PrimaryKey(autoGenerate = true) — auto-incrementing ID
  • @ColumnInfo(name = "col") — custom column name
  • @Ignore — skip property from table

DAO:

  • @Query — custom SQL with :param placeholders
  • @Insert(onConflict = REPLACE) — insert or replace
  • @Update — update by primary key
  • @Delete — delete by primary key
  • Return Flow<T> for reactive queries
  • Use suspend for write operations

Database:

  • Extend RoomDatabase()
  • Mark with @Database(entities, version)
  • Singleton pattern with Room.databaseBuilder
  • Add migrations with .addMigrations(...)

Migrations:

  • Migration(fromVersion, toVersion) — manual SQL
  • AutoMigration(from, to) — Room-generated
  • Test with MigrationTestHelper

Type Converters:

  • @TypeConverter on functions
  • @TypeConverters(Converters::class) on database