Room is the official Object Relational Mapping library for Android, developed and maintained by Google. It enables you to use the local, on-device SQLite database with less boilerplate code and offers runtime safety and a wide range of advanced features.
This tutorial explains how you can set up Room in your Android application and perform the basic Create, Read, Update and Delete (CRUD) operations.
Direct Integration with a Database in Android Apps
Android devices come preinstalled with SQLite3 database. It’s a lightweight relational database that “speaks” SQL language. To understand why most modern Android projects use Room, let me briefly describe the pain of working with Android’s database directly (the term “directly” might not be the most accurate here because you’d still use helper classes from the Android framework, but I didn’t find a better name).
Imagine that you have this data class that stores information about app’s memory consumption:
data class AppMemoryInfo( val id: Long, val consumedMemory: Float, )
You can use this data structure in your code, but, if you’d like to serialize this information into the database, you’d have to perform these steps, at the very least:
- Define and initialize
SQLiteOpenHelper
object. - Use raw SQL code to create a new table in
SQLiteOpenHelper.onCreate()
method. - Manually serialize
AppMemoryInfo
intoContentValues
data structure, and insert it into the database.
Steps #2 and #3 above involve quite a bit of code, and the amount of required code will grow linearly with the number of properties in AppMemoryInfo
data structure. Even bigger problem is that the raw SQL code and the serialization logic must be kept in sync with the implementation of AppMemoryInfo manually, by you.
Then, when you’d want to deserialize the stored information back from the database, you’d have to go through these steps:
- Obtain an instance of
Cursor
object from the database. - Iterate over the entries stored in the
Cursor
and pull the stored values from it, one by one. - Instantiate new
AppMemoryInfo
using the values from the previous step.
Just like with serialization, deserialization involves much code that must be kept in sync with the SQL that defines the database manually.
Clearly, this approach of working with the database directly is non-optimal. That’s where Room comes to the rescue.
Setting Up a Database with Room
Now I’ll show you how to set up a database using Room in your Android project. This is a simple series of steps that you’ll need to perform just once.
First, add Room’s dependency into your module’s build.gradle
file (info about the latest version here):
implementation "androidx.room:room-runtime:2.6.0" ksp "androidx.room:room-compiler:2.6.0" // kapt "androidx.room:room-compiler:2.6.0" // use this instead of ksp if you haven't migrated to KSP yet // apt "androidx.room:room-compiler:2.6.0" // use this in Java projects implementation "androidx.room:room-ktx:2.6.0" // needed if you use Coroutines
Room had started as an annotation processor and later added support for Kotlin Symbol Processing plugin. As of today, you can use both modes of operation, with KSP being the recommended one for Kotlin projects.
Sync the project and then add MyRoomDatabase
class to represent the database:
@Database( entities = [], version = 1 ) abstract class MyRoomDatabase : RoomDatabase() { }
@Database
annotation lets Room know that MyRoomDatabase
is a proxy to the underlying database and allows you to specify database configuration. Currently, the underlying database will have no tables (entities = []
) and will have version number of 1.
Instantiate MyRoomDatabase
in your Application.onCreate()
method (or inside the composition root if you’re using Dependency Injection):
lateinit var myRoomDatabase: MyRoomDatabase override fun onCreate() { super.onCreate() myRoomDatabase = Room.databaseBuilder( this, MyRoomDatabase::class.java, "MY_DATABASE_NAME" ).build() }
At this point, you have a reference to MyRoomDatabase
object and the initial setup is complete.
Creating New Database Table with Room
When using Room, you don’t create database tables using SQL statements. Instead, Room will derive the scheme of the database from special “entity” classes. To turn the previously shown AppMemoryInfo
class into Room “entity”, add these annotations:
@Entity(tableName = "appMemoryInfo") data class AppMemoryInfo( @PrimaryKey val id: Long, val consumedMemory: Float, )
@Entity
annotation makes Room recognize this class as a “template” for a database table named appMemoryInfo
. Room will automatically infer the names and the types of this table’s columns based on AppMemoryInfo
‘s properties. @PrimaryKey
annotation instructs Room to designate id
column as the primary key. I’ll discuss primary keys a bit later, but, at a very high level, this makes the database enforce that every row has a unique id
value.
As I already said, these annotations in AppMemoryInfo
class replace a bunch of SQL statements that create the respective table. More importantly, since Room infers the names and the types of the data in that table from class’ properties, you don’t need to keep these disjoint parts of code in sync yourself. For example, if you’d like to add a new column in that table, you’d just add a new property inside AppMemoryInfo
and Room would take care of the rest.
Once you defined an “entity”, you need to associate it with a specific database. So, add the AppMemoryInfo
‘s class into the entities
array inside MyRoomDatabase
:
@Database( entities = [AppMemoryInfo::class], version = 1 ) abstract class MyRoomDatabase : RoomDatabase() { }
At this point, if you build and run your application, Room will create a new database called MY_DATABASE_NAME
with a table called appMemoryInfo
. This table will have two columns: a) id
column of type INTEGER 2) consumedMemory
column of type REAL.
Note that we haven’t written even one line of SQL so far and that appMemoryInfo
table mirrors the structure of AppMemoryInfo
class automatically. That’s the power of Room.
Primary Key and Auto-Generate (Autoincrement)
Every table in SQLite3 database is created with ROWID column by default. This column contains the index number of the respective row in the table. As you insert entries, the database automatically computes and assigns unique ROWID values. In general, newer entries will have higher values of ROWID, but, if you delete an existing entry, then its row number can be reused by the database in the future (the database attempts to avoid having empty rows).
As we saw earlier, if you annotate a property inside your Room entity with @PrimaryKey annotation, it will become the primary key of the respective database table. What’s the relationship between the default ROWID column and the primary key column?
If the primary key is of any type other than Int or Long, then a new column of the respective type will be added alongside ROWID column. However, when you use Int or Long as your primary key, the name of your primary key column will become an alias to ROWID.
Earlier I said that ROWID values from deleted entries can be reused for future insertions. The database will basically try to “fill the space” left in the table after deletions. However, in some situation, this behavior might be undesirable. In these cases, you can use “auto generated” primary keys:
@Entity(tableName = "appMemoryInfo") data class AppMemoryInfo( @PrimaryKey(autoGenerate = true) val id: Long, val consumedMemory: Float, )
The addition of autoGenerate = true
will make the underlying database’s primary key AUTOINCREMENT
, which will ensure that the database generates monotonically increasing ROWID values for new entries. Please note that, as of today, Room’s official documentation states that the value of autoGenerate
controls “Whether the primary key should be auto-generated by SQLite or not”. That is incorrect, as the ROWID can be auto-generated irrespective of this flag.
In my opinion, in Android apps, if you use primary keys of type Int or Long, you should default to making them AUTOINCREMENT
.
Inserting Entities Into Database Table With Room
Room uses so-called Data Access Objects (DAOs) to handle Create Rread Update Delete (CRUD) operations. As a good practice, you’ll define a standalone DAO for each type of entity in your application.
To handle insertion of AppMemoryInfo
entities, we’ll define this DAO:
@Dao interface AppMemoryInfoDao { // Regular insertions @Insert suspend fun insert(entity: AppMemoryInfo) @Insert suspend fun insert(entities: List<AppMemroyInfo>) // Upsertions (update or insert) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(entity: AppMemoryInfo) @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsert(entities: List<AppMemoryInfo>) }
Room DAO is an interface annotated with @Dao
annotation. Based on the contents of this interface, Room will generate the respective implementation that will actually handle the required operations.
The first pair of methods in AppMemoryInfoDao
(they are suspending because I use Coroutines for concurrency) define regular insertions of either a single entity, or a list of entities. The second pair are so-called “upsertions”. The difference is that regular insertions will fail if you try to insert an entity with an existing primary key, whereas upsertion will see this operation as an update of the existing entry. In my experience, I use upsertions almost exclusively.
Once we have this DAO interface, we need to associate it with MyRoomDatabase
:
@Database( entities = [AppMemoryInfo::class], version = 1 ) abstract class MyRoomDatabase : RoomDatabase() { abstract val appMemoryInfoDao: AppMemoryInfoDao }
This instructs Room to generate an implementation of AppMemoryInfoDao
. Then you can grab a reference to this DAO and use it in your code:
myRoomDatabase.appMemoryInfoDao.upsert(AppMemoryInfo(0, consumedMemory))
When you use a primary key of type Int or Long and pass 0 as its value, Room will auto-generated a new value for the primary key colum. Therefore, the above statement will always result in addition of a new entry inside the database table. If you need to update an existing entry, just use its respective primary key value.
Reading Entities from Database Table with Room
As I said earlier, it is a good practice to use one DAO object for all the CRUD operations related to a specific entity. Therefore, we’ll add “read” methods into the same AppMemoryInfoDao
:
@Dao interface AppMemoryInfoDao { // ... other methods @Query("SELECT * FROM appMemoryInfo") suspend fun fetchAll(): List<AppMemoryInfo> @Query("SELECT * FROM appMemoryInfo WHERE id = :id") suspend fun fetchById(id: Long): AppMemoryInfo }
The first “fetch” method returns a list with all the entries form appMemoryInfo
table. The second method looks for an entry with the corresponding ID.
Please note that for queries we do write SQL code. While this might look regressive on the first sight, I actually think that’s one of the best Room’s features. In my opinion, too many other ORMs invent their own query DSLs which always grow to become very complex. So, I prefer learning a bit of SQL instead, which is universally applicable language.
Furthermore, while we do write a bit of manual SQL for queries, this experience is much better than working without Room at all. For example, if you’d write a name of non-existent table or non-existent column, Room would alert you about that on the fly, even before full code compilation. Room can also catch many errors in your SQL code. So, working with Room is safer and the feedback loop is much shorter.
Deleting Entities from Database with Room
To implement deletions, let’s add these methods to AppMemoryInfoDao
:
@Dao interface AppMemoryInfoDao { // ... other methods @Query("DELETE FROM appMemoryInfo") suspend fun deleteAll() @Query("DELETE FROM appMemoryInfo WHERE id = :id") suspend fun deleteById(id: Long) }
I think this code should be self-explanatory at this point.
Custom Column Names
There is one not strictly required practice related to Room that I think should be adopted universally. I’m talking about decoupling between the column names in the database and property names in the entity classes.
To remind you, this class:
@Entity(tableName = "appMemoryInfo") data class AppMemoryInfo( @PrimaryKey(autoGenerate = true) val id: Long, val consumedMemory: Float, )
will yield a table with columns called id
and consumedMemory
.
Now, in some cases, you might want to change the names of the properties in AppMemoryInfo
. Maybe the requirements expanded, or you just found a more descriptive name for what this property already represents. You can use Android Studio built-in refactoring capabilities to change the name across the entire codebase in just several clicks. Unfortunately, even though this is a very common and safe refactoring in general, in this case you might run into a serious issue.
The problem is that by the time you change the name of this property, your table can already contain data in the column with the previous name. Room will not make an automatic association between the new column and the existing one, therefore, for all practical purposes, by changing the name of the property you can lose database consistency.
One way to address this issue is to not rename Room entity classes, ever. I find this approach very limiting because I constantly change names in my code. Furthermore, when working on a team, this convention will be difficult to enforce.
The better solution is to decouple between the names of the columns and the names of the properties in this manner:
@Entity(tableName = "appMemoryInfo") data class AppMemoryInfo( @field:ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Long, @field:ColumnInfo(name = "consumedMemory") val consumedMemory: Float, )
Now the column names’ will be derived from the values of @ColumnInfo
annotations. Whatever you do with the names of the properties will have no effect on the database schema. So, if, for example, I want to state that the consumed memory is measured in kB (will make the life of future maintainers simpler), I can just rename the property:
@Entity(tableName = "appMemoryInfo") data class AppMemoryInfo( @field:ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Long, @field:ColumnInfo(name = "consumedMemory") val consumedMemoryKb: Float, )
The name of the column in the database will remain consumedMemory and Room will keep associating the renamed property with this existing database column.
Conclusion
Alright, that’s it for this post. Now you know how to set up Room Object-Relational Mapper in your Android project and perform the standard CRUD operations with it.
In the next articles in this series I’ll discuss more advanced Room concepts and tricks. Don’t forget to subscribe to get notifications about new posts right into your inbox.
Hi Vasiliy, all this is exposed in a very concise and understandable manner. I learned some tricks. Waiting for the other Part. Regards.