Getting Started with Room Kotlin API

  • 2021-12-12 05:44:18
  • OfStack

Directory definition database table Accessing data in a table Insert data Query data Create a database Test Dao

Room is the encapsulation of SQLite, which makes it easy for Android to manipulate the database and is by far my favorite Jetpack library. In this article, I will show you how to use and test Room Kotlin API, and I will share how it works during the introduction.

We will explain it based on Room with a view codelab. Here we will create a vocabulary stored in the database, then display them on the screen, and users can also add words to the list.

Define database tables

There is only one table in our database, that is, the table that holds vocabulary. The Word class represents one record in the table, and it requires the annotation @ Entity. We use the @ PrimaryKey annotation to define the primary key for the table. Then, Room generates an SQLite table with the same table name as the class name. Members of each class correspond to columns in the table. The column name and type correspond to the name and type 1 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 with 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)

We recommend using the @ ColumnInfo annotation because it gives you more flexibility to rename members without modifying the database column names at the same time. Because modifying column names involves modifying database schemas, you need to implement data migration.

Accessing data in a table

To access the data in the table, you need to create a data access object (DAO). That is, an interface called WorkDao, which will be annotated with @ Dao. 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. Manipulating the database is a time-consuming I/O operation, so it needs to be done in a background thread. We will combine Room with Kotlin and Flow to achieve the above functions.


/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

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

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

We introduced the basic concepts of synergy in the video Kotlin Vocabulary, and introduced the related contents of Flow in another video of Kotlin Vocabulary.

Insert data

To implement the operation of inserting data, first create an abstract pending function with the inserted words as its parameters and add the @ Insert annotation. Room generates the entire operation of inserting data into the database, and since we define the function as suspendable, Room puts the entire operation in a background thread. Therefore, the suspended function is thread-safe, meaning that the main thread can be invoked without fear of blocking the main thread.


@Insert
suspend fun insert(word: Word)

The implementation code of Dao abstract function is generated in the bottom Room. The following code snippet is the concrete 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);
}

The CoroutinesRoom. execute () function is called with three arguments: the database, an identity to indicate whether or not you are in a transaction, and an Callable object. Callable. call () contains code for handling database insert operations.

If we look at the implementation of CoroutinesRoom. execute (), we will see that Room moves callable. call () to another CoroutineContext. This object comes from the executor you provided when you built the database, or uses Architecture Components IO Executor by default.

Query data

In order to be able to query table data, we create an abstract function here and add @ Query annotation to it, followed by SQL request statement: this statement requests all words from the word data table and sorts them in alphabetical order.

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


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

At the bottom layer, 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();
    }
  });
}

We can see that the code calls CoroutinesRoom. createFlow (), which contains four parameters: the database, a variable to identify whether we are in a transaction, a list of database tables to listen to (in this case, only word_table in the list), and an Callable object. Callable. call () contains the implementation code for the query that needs to be triggered.

If we look at the implementation code of CoroutinesRoom. createFlow (), we will find that a different CoroutineContext is used here than the data request call 1. As with data insertion call 1, the dispenser here comes from the executor you provided when you built the database, or from the Architecture Components IO executor used by default.

Create 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 add the @ Database annotation. Pass in Word as the entity element to be stored, and the numeric value 1 as the database version.

We will also define an abstract method that returns an WordDao object. All of these are abstract types, because Room generates all the implementation code for us. Just like here, there is a lot of logic code that we don't need to implement personally.

The last step is to build the database. We want to be able to ensure that there are not multiple instances of the database open at the same time and that the context of the application is required to initialize the database. One implementation method is to add a companion object to the class, define an RoomDatabase instance in it, and then add an getDatabase function to the class to build the database. If we want the Room query to be executed not in IO Executor created by Room itself, but in another Executor, we need to pass the new Executor into 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 {
    return INSTANCE ?: synchronized(this) {
      val instance = Room.databaseBuilder(
        context.applicationContext,
        WordRoomDatabase::class.java,
        "word_database"
        ).build()
      INSTANCE = instance
      //  Returning an instance 
      instance
    }
  }
}

Test Dao

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

When implementing Dao tests, we create a database before each test runs. When each test runs, we shut down the database. Since we don't need to store data on the device, we can use an in-memory database when creating a database. Because this is just 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() {
      val context: Context = ApplicationProvider.getApplicationContext()
      //  Because the data here will be cleared when the process ends, the in-memory database is used 
      db = Room.inMemoryDatabaseBuilder(context, WordRoomDatabase::class.java)
          //  Requests can be initiated in the main thread for testing only. 
          .allowMainThreadQueries()
          .build()
      wordDao = db.wordDao()
  }

  @After
  @Throws(IOException::class)
  fun closeDb() {
      db.close()
  }
...
}

To test whether words are 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 will run the test in the runBlocking code block. Because this is just a test, we don't have to worry about whether the test process will block the test thread.


/* Copyright 2020 Google LLC.  
   SPDX-License-Identifier: Apache-2.0 */

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

In addition to the functionality described in this article, Room provides a lot of functionality and flexibility that goes far beyond the scope of this article. For example, you can specify how Room handles database conflicts, create TypeConverters to store data types that native SQLite cannot store (such as Date type), use JOIN and other SQL features to implement 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. If you want to learn through practice, you can visit Room with a view codelab.

The above is the Room Kotlin API using the details of the introduction tutorial, more information about Room Kotlin API use please pay attention to other related articles on this site!


Related articles: