Using and testing Room Kotlin APIs
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
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.
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.
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.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.
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.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
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.