Start with the Room Kotlin API

Keywords: Android kotlin room

Room It is the encapsulation of SQLite, which makes the operation of Android database very simple. It is also my favorite Jetpack library so far. In this article, I will tell you how to use and test the Room Kotlin API, and I will share its working principle during the introduction.

We will be based on Room with a view codelab Explain it to you. Here we will create a glossary stored in the database and display them on the screen. At the same time, users can 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 a record in the table, and it needs to use the annotation @ Entity. We use the @ PrimaryKey annotation to define the primary key for the table. Then, Room will generate a SQLite table with the same table name and class name. The members of each class correspond to the columns in the table. The column name and type are consistent with the name and type of each field in the class. If you want to change the column name without using the variable name in the class as the column name, you can modify it through 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 that you use the @ ColumnInfo annotation because it allows you to rename members more flexibly without modifying the column names of the database at the same time. Because modifying column names involves modifying the database schema, 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 implement table level data insertion, deletion and acquisition through it, so the corresponding abstract methods will be defined in the data access object. Database operation is a time-consuming I/O operation, so it needs to be completed in the background thread. We will combine Room with Kotlin collaboration and Flow to realize 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 it in the video Kotlin Vocabulary Related basic concepts of collaborative process
In another video of Kotlin Vocabulary Flow related content.

insert data

To implement the operation of inserting data, first create an abstract suspend function, take the inserted word as its parameter, and add the @ Insert annotation. Room will generate all operations of inserting data into the database, and since we define the function as suspended, room will complete the whole operation process in the background thread. Therefore, the suspended function is safe for the main thread, that is, it can be called safely in the main thread without worrying about blocking the main thread.

@Insert
suspend fun insert(word: Word)

The implementation code of Dao abstract function is generated in the underlying Room. The following code fragment is the specific 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, which contains three parameters: database, an ID indicating whether it is in a transaction, and a callable object. Callable.call() contains code to handle database insert data operations.

If we look at CoroutinesRoom.execute() realization , we'll see Room move label. Call () to another CoroutineContext. The object comes from the executor you provided when building the database, or Architecture Components IO Executor is used by default.

Query data

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

We want to be notified when the data in the database changes, so we return a Flow < list < word > >. Since the return type is Flow, Room will execute data requests in the background thread.

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

At the bottom layer, Room generates getalphabetized words ():

/* 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 CoroutinesRoom.createFlow() is called in the code, which contains four parameters: database, a variable used to identify whether we are in a transaction, a list of database tables to listen to (in this example, there is only word_table in the list), and a callable object. Callable.call() contains the implementation code of the query that needs to be triggered.

If we look at CoroutinesRoom.createFlow() Implementation code , you will find that different CoroutineContext is used here as in the data request call. Like the data insertion call, the distributor here comes from the executor you provided when building the database, or from the Architecture Components IO executor used by default.

Create database

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

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

The last step is to build the database. We want to ensure that there are no multiple database instances open at the same time, and we also need the application context to initialize the database. One implementation method is to add an associated object in the class, define a RoomDatabase instance in it, and then add the getDatabase function in 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 call setQueryExecutor() Pass the new Executor into the builder.

/* 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
      // Return instance
      instance
    }
  }
}

Test Dao

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

When implementing Dao tests, we create a database before each test run. After each test run, we shut down the database. Since we do not need to store data on the device, we can use an in memory database when creating a database. Also 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()
      // Since 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 purposes only.
          .allowMainThreadQueries()
          .build()
      wordDao = db.wordDao()
  }

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

To test whether the Word can be added to the database correctly, we will create a Word instance, insert it into the database, find the first Word in the Word list in alphabetical order, and then ensure that it is consistent with the Word we created. Since we call the suspend function, we will run the test in the runBlocking code block. Because this is just a test, we don't need to care 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 functions described in this article, Room provides a lot of functionality and flexibility, which is far beyond the scope of this article. For example, you can specify how Room handles database conflicts, and you can create TypeConverters to store data types that native SQLite cannot store (such as Date type ). you can use JOIN and other SQL functions to realize complex queries Create database view , pre populate the database, and trigger specific actions when the database is created or opened.

For more information, please refer to our Room official document , if you want to learn through practice, you can visit Room with a view codelab.

Posted by shruti on Wed, 10 Nov 2021 19:24:16 -0800