Room is a wrapper around SQLite that makes it very easy for Android to manipulate databases and is by far my favorite Jetpack library. In this article I’ll show you how to use and test the Room Kotlin API, and I’ll share how it works during the introduction.

We will talk about it based on Room with a View Codelab. Here we create a glossary stored in the database and display them on the screen, while the user can add words to the list.

Defining database tables

There is only one table in our database, and that is the table that holds the terms. The Word class represents a record in a table, and it requires the annotation @Entity. We use the @primarykey annotation to define the PrimaryKey for the table. Room then generates an SQLite table with the same name as the class. Members of each class correspond to columns in the table. The column name and type are the same as the name and type of each field in the class. If you want to change the column name instead of using the variable name in the class as the column name, you can do so using the @ColumnInfo annotation.

/* Copyright 2020 Google LLC.spdx-license-Identifier: Apache-2.0 */

@Entity(tableName = "word_table")
data class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
Copy the code

The @ColumnInfo annotation is recommended because it gives you more flexibility in renaming members without simultaneously changing the column names of the database. Because changing column names involves changing the database schema, you need to implement data migration.

Access the data in the table

To access data in a table, you need to create a data access Object (DAO). That is, an interface called WorkDao, which will have @DAO annotations. We want to use it to insert, delete, and retrieve data at the table level, so the corresponding abstract methods are defined in the data access object. Operating a database is a time-consuming I/O operation, so it needs to be done in a background thread. We will combine Room with Kotlin coroutines and Flow to do this.

/* Copyright 2020 Google LLC.spdx-license-Identifier: Apache-2.0 */

@Dao
interface WordDao {
    @Query("SELECT * FROM word_table ORDER BY word ASC")
    fun getAlphabetizedWords(a): Flow<List<Word>>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(word: Word)
}
Copy the code

We introduced the basic concepts related to coroutines in the video Kotlin Vocabulary, and introduced the content related to Flow in another video of Kotlin Vocabulary.

Insert data

To implement the Insert operation, first create an abstract suspend function that takes the inserted word as an argument and adds an @INSERT annotation. Room generates all the operations that insert data into the database, and since we’ve defined the function to be suspended, Room puts the entire operation in a background thread. Therefore, the suspended function is mainline-safe, meaning that it can be called from the main thread without worrying about blocking the main thread.

@Insert
suspend fun insert(word: Word)
Copy the code

The implementation code for the Dao abstract function is generated in the underlying Room. The following code snippet is an implementation of our data insertion method:

/* Copyright 2020 Google LLC.spdx-license-Identifier: Apache-2.0 */

@Override
public Object insert(final Word word, final Continuation<? super Unit> p1) {
    return CoroutinesRoom.execute(__db, true, new Callable<Unit> () {@Override
      public Unit call() throws Exception {
          __db.beginTransaction();
          try {
              __insertionAdapterOfWord.insert(word);
              __db.setTransactionSuccessful();
          return Unit.INSTANCE;
          } finally {
              __db.endTransaction();
          }
      }
    }, p1);
}
Copy the code

The coroutinesroom.execute () function is called with three parameters: the database, an identifier to indicate whether a transaction is in progress, and a Callable object. Callable.call() contains code to handle database insertion operations.

If we look at the implementation of coroutinesRoom.execute (), we see that Room moves callable.call() to another CoroutineContext. This object comes from the Executor that you provide when you build the database, or the Architecture Components IO Executor is used by default.

Query data

To be able to Query the table data, we create an abstract function and add an @Query annotation to it, which is followed by an SQL request statement that requests all the words from the word table in alphabetical order.

We want to be notified when data in the database changes, so we return a Flow >. Since the return type is Flow, Room performs the data request in the background thread.

@query (" SELECT * FROM word_table ORDER BY word ASC ")
fun getAlphabetizedWords(a): Flow<List<Word>>
Copy the code

At the bottom, Room generates getAlphabetizedWords():

/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ @override public Flow<List<Word>> getAlphabetizedWords() {final String _sql = "SELECT * FROM word_table ORDER BY word ASC"; final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0); return CoroutinesRoom.createFlow(__db, false, new String[]{"word_table"}, new Callable<List<Word>>() { @Override public List<Word> call() throws Exception { final Cursor _cursor = DBUtil.query(__db, _statement, false, null); try { final int _cursorIndexOfWord = CursorUtil.getColumnIndexOrThrow(_cursor, "word"); final List<Word> _result = new ArrayList<Word>(_cursor.getCount()); while(_cursor.moveToNext()) { final Word _item; final String _tmpWord; _tmpWord = _cursor.getString(_cursorIndexOfWord); _item = new Word(_tmpWord); _result.add(_item); } return _result; } finally { _cursor.close(); } } @Override protected void finalize() { _statement.release(); }}); }Copy the code

We can see the code calls the CoroutinesRoom. CreateFlow (), it contains four parameters: Database, a variable that identifies whether we are in a transaction, a list of database tables to listen on (in this case, word_table is the only one in the list), and a Callable object. Callable.call() contains the implementation code for the query that needs to be triggered.

If we look at the CoroutinesRoom. CreateFlow () the implementation of the code, will find that here as well as data request call using different CoroutineContext. As with the data insert call, the dispenser here comes from the executor you provided when building the database, or from the Architecture Components IO executor used by default.

Creating a database

Now that we have defined the data stored in the database and how to access it, let’s define the database. To create the Database, we need to create an abstract class that inherits from RoomDatabase and adds the @Database annotation. Word is passed in as the entity element to be stored, with the value 1 as the database version.

We’ll also define an abstract method that returns a WordDao object. All of this is abstract because Room will generate all of the implementation code for us. Just like here, there’s a lot of logical code that we don’t have to implement ourselves.

The last step is to build the database. We want to be able to ensure that we don’t have multiple database instances open at the same time, and we also need the context of the application to initialize the database. One way to do this is to add companion objects to the class, define an instance of RoomDatabase in it, and then add the getDatabase function to the class to build the database. If we want the Room query to be executed not in the IO Executor created by Room itself, but in another Executor, we need to pass the new Executor into the Builder by calling setQueryExecutor().

/* Copyright 2020 Google LLC.spdx-license-Identifier: Apache-2.0 */

companion object {
  @Volatile
  private var INSTANCE: WordRoomDatabase? = null
  fun getDatabase(context: Context): WordRoomDatabase {
    returnINSTANCE ? : synchronized(this) {
      val instance = Room.databaseBuilder(
        context.applicationContext,
        WordRoomDatabase::class.java,
        "word_database"
        ).build()
      INSTANCE = instance
      // Return the instance
      instance
    }
  }
}
Copy the code

Test the Dao

To test the Dao, we need to implement the AndroidJUnit test to let Room create the SQLite database on the device.

When implementing Dao tests, we create the database before each test runs. After each test runs, we close the database. Since we don’t need to store data on the device, we can use the in-memory database when creating the database. Also because this is only a test, we can run the request in the main thread.

/* Copyright 2020 Google LLC.spdx-license-Identifier: Apache-2.0 */

@RunWith(AndroidJUnit4::class)
class WordDaoTest {
  
  private lateinit var wordDao: WordDao
  private lateinit var db: WordRoomDatabase

  @Before
  fun createDb(a) {
      val context: Context = ApplicationProvider.getApplicationContext()
      // The in-memory database is used since the data is cleared when the process terminates
      db = Room.inMemoryDatabaseBuilder(context, WordRoomDatabase::class.java)
          // Requests can be made in the main thread for testing purposes only.
          .allowMainThreadQueries()
          .build()
      wordDao = db.wordDao()
  }

  @After
  @Throws(IOException::class)
  fun closeDb(a) {
      db.close()
  }
...
}
Copy the code

To test if a Word can be correctly added to the database, we create an instance of Word, insert it into the database, find the first Word in the list alphabetically, and make sure it matches the Word we created. Since we are calling a pending function, we run the test in a runBlocking block. Since this is just a test, we don’t need to care if the test is blocking the test thread.

/* Copyright 2020 Google LLC.spdx-license-Identifier: Apache-2.0 */

@Test
@Throws(Exception::class)
fun insertAndGetWord(a) = runBlocking {
    val word = Word("word")
    wordDao.insert(word)
    val allWords = wordDao.getAlphabetizedWords().first()
    assertEquals(allWords[0].word, word.word)
}
Copy the code

In addition to the features covered in this article, Room offers a great deal of functionality and flexibility that goes far beyond the scope covered in this article. For example, you can specify how Room handles database conflicts, you can create TypeConverters to store data types (such as Date types) that native SQLite cannot store, and you can use joins and other SQL Features enable complex queries, create database views, pre-populate the database, and trigger specific actions when the database is created or opened.

For more information, please refer to our official Room documentation. For hands-on learning, visit Room with a View Codelab.