Using and testing Room Kotlin APIs

Florina Muntenescu
Android Developers
Published in
5 min readJan 5, 2021

--

Room is a wrapper over SQLite and it makes working with databases on Android so much easier and is by far my favorite Jetpack library. In this post I want to tell you how to use and test Room Kotlin APIs and while we do that, I’ll also share how things work under the hood.

We’re going to use the Room with a view codelab as a basis. Here we’re building a list of words that are saved in the database and displayed on the screen. The user can also add words to the list.

Define the database table

In our database we have only one table: the one containing words. The Word class represents an entry in the table and it needs to be annotated with @Entity. We use @PrimaryKey to define the primary key for the table. Based on this, Room will generate a SQLite table, with the same name as the class name. Each member of the class becomes a different column in the table. The column name and type is given by the name and type of each field. You can change it by using the @ColumnInfo annotation.

We recommend you always use the @ColumnInfo annotation as it gives you more flexibility to rename the members without having to change the database column names. Changing the column names leads to a change in the database schema and therefore you need to implement a migration.

Access the data in the table

To access the data in the table we create a data access object (DAO). This will be an interface called WordDao, annotated with @Dao. We want to insert, delete and get data from the table so these will be defined as abstract methods in the DAO. Working with the database is a time consuming I/O operation, so this needs to be done on the background thread. We’ll use Room integration with Kotlin coroutines and Flow to achieve this.

We’ve covered the basics of working with coroutines in this Kotlin vocabulary video. Check out this video for information about Flow.

Insert data

To insert data create an abstract suspend function that gets as parameter the Word to be inserted and annotate it with @Insert. Room will generate all the work that needs to be done to insert the data in the database and, because we made the function suspend, Room moves all the work to be done to a background thread. Thus, this suspend function is main-safe: safe to be called from the main thread.

@Insert
suspend fun insert(word: Word)

Under the hood Room generates the implementation of the Dao. Here’s how the implementation of our insert method looks like:

CoroutinesRoom.execute() function is called, with 3 parameters: the database, a flag to indicate whether we’re in a transaction and a Callable object. Callable.call() contains the code that handles the database insertion.

If we check the CoroutinesRoom.execute() implementation, we see that Room moves callable.call() to a different CoroutineContext. This is derived from the executors you provide when building your database or by default will use the Architecture Components IO Executor.

Query data

To query the table, we’ll create an abstract function and annotate it with @Query, passing in the SQL query needed to get the data we want: all words from the word table, ordered alphabetically.

We want to be notified whenever the data in the database changes, so we return a Flow<List<Word>>. Because of the return type, Room runs the query on the background thread.

@Query(“SELECT * FROM word_table ORDER BY word ASC”)
fun getAlphabetizedWords(): Flow<List<Word>>

Under the hood, Room generates the getAlphabetizedWords() for us:

We see a CoroutinesRoom.createFlow() call, with 4 parameters: the database, a flag indicating whether we’re in a transaction, the list of tables that should be observed for changes (in our case just word_table) and a Callback. Callback.call() contains the implementation of the query to be triggered.

If we check the CoroutinesRoom.createFlow() implementation, we see that here as well the query call is moved to a different CoroutineContext. Like with the insert call, this dispatcher is derived from the executors you provide when building your Database or by default will use the Architecture Components IO executor.

Create the database

We’ve defined the data to be stored in the database and how to access it, now it’s time to define the database itself. For this, we create an abstract class that extends RoomDatabase, annotate it with @Database, passing in Word as the entity to be stored, and 1 as database version.

We’ll define an abstract method that returns the WordDao. Everything is abstract because Room is the one that generates the implementation for us. Like this, there’s a lot of logic that we no longer need to implement.

The only step left is to build the database. We want to make sure we don’t have multiple database instances open at the same time, and we need the application context to initialise the database. So one way to handle this is to define a RoomDatabase instance in the companion object of our class, and a getDatabase function that builds the database. If we want Room queries to be executed on a different Executor than the IO Executor created by Room, we can pass it in the builder by calling setQueryExecutor().

Testing the Dao

To test the Dao we need to implement an AndroidJUnit test as Room creates an SQLite database on the device.

When implementing the Dao test, before each test is run, we create the database. After each test is run, we close the database. As we don’t need to save the data on the device, when creating the database, we can use an in-memory one. As this is a test, we can allow queries to be run on the main thread.

To test if a word is inserted successfully, we’ll create a word, insert it, get the first word from the list of alphabetised words and ensure it’s the same as the word we created. As we’re calling suspend functions, we’ll run the test in a runBlocking block. Since this is a test, we don’t mind if the test blocks the test thread.

Room offers a lot more functionality and flexibility than what we’ve covered — you can define how Room should handle database conflicts, you can store types that otherwise, natively with SQLite can’t be stored, like Date, by creating TypeConverters, you can implement complex queries, using JOIN and other SQL functionality, create database views, pre-populate your database or trigger certain database actions whenever the database is created or opened.

Check out our Room documentation for more information and the Room with a view codelab for hands-on learning.

--

--