Room is an ORM library from Android Jetpack that provides an abstraction layer over SQLite for working with local databases on Android. According to the official documentation at Android Developers, 2025, Room automatically generates DAO implementations based on annotations at compile time, eliminating about 70% of boilerplate code compared to direct SQLiteOpenHelper usage. The library performs SQL query validation at compile time, allowing developers to catch syntax errors before launching the application on a device.
Key Takeaways
Room is an ORM library from Android Jetpack created by Google to simplify working with local SQLite databases on the Android platform. It provides annotations for describing the data schema and automatically generates DAO interface implementations at compile time. Unlike direct SQLiteOpenHelper usage, Room frees the developer from writing a significant amount of boilerplate code for creating, opening and managing database connections.
The library was introduced at Google I/O 2017 as part of Android architecture components. Since then, Room has become the de facto standard for local data storage, surpassing solutions like GreenDAO and Realm for Android in popularity. According to Google, the library is used in more than 60% of applications published on Google Play that work with local data on the device.
The key feature is SQL query validation at compile time using an annotation processor. If a developer makes an error in an SQL command, for example, specifying a non-existent column name, the build will fail with an error before the application is installed. This is fundamentally different from the SQLiteOpenHelper approach, where such errors are only detected at runtime, often in production.
SQLite supports only five data types: TEXT, INTEGER, REAL, BLOB and NULL. However, Java and Kotlin use complex types: Date, List, Enum and custom objects. To store them, Room provides the TypeConverters mechanism — static methods that convert a complex type into a primitive type understandable by SQLite. For example, a Date object is converted to Long (timestamp), and List<String> to a JSON string via Gson or Moshi.
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
val db = Room
.databaseBuilder(context, AppDatabase::class.java, "app-db")
.build()
To declare a converter, simply add the @TypeConverter annotation to a static method and specify the converter class in the @TypeConverters annotation at the database level. Room automatically applies the converter when reading and writing the corresponding type in each SQL query without manually calling conversion methods.
Room consists of three main components: Entity, DAO and Database. Each performs a strictly defined role and is annotated with the corresponding annotation. Together, they form a complete data access layer that isolates the application's business logic from SQLite implementation details.
Entity is a data class that describes the structure of one table in the database. Each class field corresponds to a table column, and each row in the database corresponds to one instance of the class. The @Entity annotation tells Room that the class is a table. The field with the @PrimaryKey annotation defines the primary key, which can be auto-incremented or composite. @ForeignKey is used for relationships between tables, ensuring data integrity at the database level.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "full_name")
val name: String,
val age: Int,
val email: String
)
DAO (Data Access Object) is an interface or abstract class that declares operations for working with data: insert, read, update and delete. Each operation is annotated with @Insert, @Query, @Update or @Delete. Room automatically generates the implementation of this interface at compile time. The @Query annotation is particularly valuable — it accepts an SQL query as a string and validates its correctness at build time.
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User): Long
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
@Delete
suspend fun delete(user: User)
}
Database is an abstract class extending RoomDatabase that serves as the entry point to the database. It contains a list of all Entities and provides abstract methods for obtaining DAOs. The class is annotated with @Database, which specifies the schema version and the list of entities. The database instance is created via Room.databaseBuilder with the application context, file name and Database class.
Room does not replace SQLite, but works on top of it as an abstraction layer. The internal architecture includes an annotation processor, code generator and connection pool. At compile time, the annotation processor analyzes the Entity, DAO and Database classes, then generates implementation classes with the _Impl suffix. All generated classes are placed in the build package and are not directly visible to the developer.
Code generation at compile time is Room's central mechanism. For each DAO interface, a class with the full implementation of all annotated methods is generated. SQL queries from the @Query annotation are validated for correctness: the processor matches column names with Entity fields and checks SQL syntax. If an error is found, the build is interrupted with a clear message. This is impossible when using raw SQLiteOpenHelper, where errors only surface at runtime.
The generation process includes three stages. First — schema validation: the processor checks that all classes listed in @Database are valid Entities. Second — DAO body generation: for each method, an implementation is created using the internal RoomSQLiteQuery object that executes prepared queries. Third — generation of the Database_Impl class, which handles database creation and opening, as well as initialization of all DAO objects.
class UserDao_Impl(private val __db: RoomDatabase) : UserDao {
private val __insertionAdapter = __db
.createInsertionAdapter(User::class, 0)
override suspend fun insert(user: User): Long {
__db.assertNotSuspendingTransaction()
return __db.runInTransaction {
__insertionAdapter.insertAndReturnId(user)
}
}
}
Room does not create a separate thread pool for database operations. By default, queries run on the calling thread with one limitation: reading and writing block the thread. For asynchronous work, Room integrates with Kotlin coroutines via suspend functions, with LiveData through return values, and with Flow through reactive wrappers. This gives the developer the flexibility to choose the architectural solution for a specific task.
Let's look at a practical example of creating a note-taking application using Room. The application contains one Note table with fields id, title, content and timestamp. Users can add, view and delete notes. Coroutines are used for asynchronous operations.
To integrate Room into an Android project, add dependencies to the module-level build.gradle file. Room requires three components: the runtime library, the kapt annotation processor and optional coroutine support. The library version is specified in a room_version variable for easy updating. Starting with Room 2.4.0, KSP is supported as an alternative to kapt with faster build speeds.
dependencies {
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// Optional: testing
testImplementation "androidx.room:room-testing:$room_version"
}
After setting up the dependencies, create three files: the Note Entity, the NoteDao interface and the AppDatabase class. The Note Entity contains fields with @PrimaryKey and @ColumnInfo annotations. The DAO provides methods for inserting, retrieving the list and deleting. The Database links the Entity and DAO through the @Database annotation.
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val content: String,
@ColumnInfo(name = "created_at")
val timestamp: Long = System.currentTimeMillis()
)
@Dao
interface NoteDao {
@Insert
suspend fun insert(note: Note)
@Query("SELECT * FROM notes ORDER BY created_at DESC")
fun getAllNotes(): Flow<List<Note>>
@Delete
suspend fun delete(note: Note)
}
The AppDatabase file is declared as an abstract class extending RoomDatabase. The @Database annotation specifies all Entities for the current version and the schema version number. To obtain an instance, the singleton pattern is used via the Room.databaseBuilder build method with the application context. Caching the database instance prevents multiple creations that could lead to memory leaks.
Migrations in Room are a mechanism for changing the database schema when updating an application without losing existing data. When a user installs a new version with modified Entities, Room detects the version mismatch and executes the specified migration steps. Without a migration, the database will be deleted and recreated, resulting in the loss of all user-saved data.
A migration is described by the Migration class, which takes the starting and ending database versions. Inside the migrate method, an ALTER TABLE or CREATE TABLE SQL query is executed to change the schema. Room cannot automatically detect schema changes — the developer must write a migration manually for each Entity change. Starting with Room 2.4.0, the experimental autoMigrations feature is available for automatic migration generation.
The autoMigrations feature allows Room to automatically generate migrations based on differences between Entity versions. To use it, simply add the @AutoMigration annotation to @Database and enable schema export to JSON. Room compares the schemas of adjacent versions and generates the necessary ALTER queries. However, autoMigrations only supports backward-compatible changes: adding columns, creating indexes and changing types with compatible transformations.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE users ADD COLUMN phone TEXT"
)
}
}
val db = Room
.databaseBuilder(context, AppDatabase::class.java, "app-db")
.addMigrations(MIGRATION_1_2)
.build()
For complex changes, such as renaming columns or merging tables, a manual migration using intermediate tables is required. A typical scenario: create a temporary table with the old schema, copy data from the old table to the new one with transformations, delete the old table and rename the temporary one. Room guarantees that all migrations are executed in a single transaction, and if an error occurs, the changes are fully rolled back.
Frequently Asked Questions
Room provides an ORM abstraction with annotations and SQL validation at compile time, while SQLiteOpenHelper requires manually writing all queries and managing connections. Room automatically generates code for CRUD operations and integrates with Android architecture components, including LiveData and Flow.
Room supports all Java primitive types: Int, Long, Boolean, Float, Double, as well as String, ByteArray and Date. For complex types such as List or Enum, TypeConverters are used — static conversion methods that convert non-standard types into SQLite-compatible formats.
Yes, Room supports synchronous calls without coroutines, but they block the thread they run on. For asynchronous work, you can use LiveData or RxJava instead of coroutines. Google recommends using coroutines as the primary method for asynchronous data access in new projects.
If Room detects a database version mismatch and cannot find a suitable migration, by default it throws an IllegalStateException with an error description. The developer can override this behavior with the fallbackToDestructiveMigration method, which will delete the existing database and create a new one, losing all data.
Room supports relationships through nested objects with the @Embedded annotation and through relation classes with the @Relation annotation. For complex queries involving table joins, custom POJO classes are used, whose fields are populated from the results of @Query with SQL JOIN statements.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also