We will cover briefly about

  1. Using Room in Jetpack Compose
  2. Writing CRUD operations
  3. Write Test for Database

Note: This article assumes the reader knows about Jetpack Compose

Using Room in Jetpack Compose

What is Room?

As per the documentation,

The Room persistence library provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite.

 Room in Jetpack
 Room in Jetpack

Setup Room in Compose

To use Room in your app, add the following dependencies to your app’s build.gradle file:

dependencies {
def room_version = "2.2.6"

implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"

Components of Room

There are 3 major components in Room:

Room Architecture
Room Architecture

Inside our code, it looks like this

Room Structure
Room Structure

Writing CRUD operations

Let’s look into what each file inside database(above screenshot) will have

  1. TodoItem (Our entity class)

We create an entity called TodoItem

  • The class is annotated with @Entity and name of the table.
@Entity(tableName = "my_todo_list")
data class TodoItem(
    @PrimaryKey(autoGenerate = true)
    var itemId: Long = 0L,
    @ColumnInfo(name = "item_name")
    val itemName: String, 

    @ColumnInfo(name = "is_completed")
    var isDone: Boolean = false

2. TodoDatabaseDao (Our Data Access Objects)

We create an interface(TodoDatabaseDao)

  • The class is annotated with @ Dao
interface TodoDatabaseDao {
    @Query("SELECT * from my_todo_list")
    fun getAll(): LiveData<List<TodoItem>>
    @Query("SELECT * from my_todo_list where itemId = :id")
    fun getById(id: Int) : TodoItem?
    suspend fun insert(item:TodoItem)
    suspend fun update(item:TodoItem)
    suspend fun delete(item:TodoItem)
    @Query("DELETE FROM my_todo_list")
    suspend fun deleteAllTodos()
  • Query Annotation is used for writing custom queries (like read or delete all in our case)
  • Annotations Insert, Update, Delete perform CUD operations.
  • We mark these functions as suspend (so that we can call them inside coroutines)

3. TodoDatabase (Our database class)

We define an abstract class(TodoDatabase) that extends RoomDatabase

  • This class is annotated with @ Database
@Database(entities = [TodoItem::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
    abstract fun todoDao(): TodoDatabaseDao
    companion object {
         private var INSTANCE: TodoDatabase? = null
         fun getInstance(context: Context): TodoDatabase {
            synchronized(this) {
                var instance = INSTANCE
                if (instance == null) {
                    instance = Room.databaseBuilder(
                 INSTANCE = instance
             return instance

  • A thread that enters a synchronized method obtains a lock and no other thread can enter the method until the lock is released. Kotlin offers the same functionality with the Synchronized annotation.

4. TodoRepository (Our repository class)

We create a repository class (TodoRepository) that takes in the TodoDatabaseDao as the constructor parameter

class TodoRepository(private val todoDatabaseDao: TodoDatabaseDao) {
    val readAllData : LiveData<List<TodoItem>> =  todoDatabaseDao.getAll()
    suspend fun addTodo(todoItem: TodoItem) {
    suspend fun updateTodo(todoItem: TodoItem) {
    suspend fun deleteTodo(todoItem: TodoItem) {
    suspend fun deleteAllTodos() {
  • All the interactions to the database are done via this repository layer
  • Since, our implementations were suspend functions, so are our definitions
  • We expose readAllData, which is of type LiveData, hence we can observe to the changes and notify UI

Write Test for Database

Since, we have our TodoDatabase ready, let’s test it

  • Create a test class TodoDatabaseTest under the androidTest folder
  • We annotate our class with AndroidJUnit4 This is the thing that will drive the tests for a single class.
class TodoDatabaseTest {

    private lateinit var todoDao: TodoDatabaseDao
    private lateinit var db: TodoDatabase

    fun createDb() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext

        db = Room.inMemoryDatabaseBuilder(context, TodoDatabase::class.java)

        todoDao = db.todoDao()

    fun deleteDb() {

    fun insertAndGetTodo() = runBlocking {
        val todoItem = TodoItem(itemId = 1, itemName = "Dummy Item", isDone = false)
        val oneItem = todoDao.getById(1)
        assertEquals(oneItem?.itemId, 1)
  • Before the test runs, we create the database using createDb
  • After the test runs, we delete the database using deleteDb
  • During the test is running, we invoke the insertAndGetTodo
  • Since this is only a test, hence we use runBlocking.
  • Create a todo and assert it against the value created
Run the test
Run the test
  • Click on Run TodoDatabaseTest to run the test
  • If everything is fine, you should see
Test results for Room
Test results for Room

Source Code

Valuable comments