We will cover briefly about
- Using Room in Jetpack Compose
- Writing CRUD operations
- Write Test for Database
Note: This article assumes the reader knows about Jetpack Compose
Using Room in Jetpack Compose
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.
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"
// FOR ROOM
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"
// END FOR ROOM
}
Components of Room
There are 3 major components in Room:
- The database class that holds the database.
- Data entities that represent tables in your app’s database.
- Data access objects (DAOs) that provide methods that your app can use to query, update, insert, and delete data in the database.
Inside our code, it looks like this
Writing CRUD operations
Let’s look into what each file inside database(above screenshot) will have
- 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
@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? @Insert suspend fun insert(item:TodoItem) @Update suspend fun update(item:TodoItem) @Delete 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(
context.applicationContext,
TodoDatabase::class.java,
"todo_list_database"
).fallbackToDestructiveMigration()
.build()
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) {
todoDatabaseDao.insert(todoItem)
}
suspend fun updateTodo(todoItem: TodoItem) {
todoDatabaseDao.update(todoItem)
}
suspend fun deleteTodo(todoItem: TodoItem) {
todoDatabaseDao.delete(todoItem)
}
suspend fun deleteAllTodos() {
todoDatabaseDao.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.
@RunWith(AndroidJUnit4::class)
class TodoDatabaseTest {
private lateinit var todoDao: TodoDatabaseDao
private lateinit var db: TodoDatabase
@Before
fun createDb() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(context, TodoDatabase::class.java)
.allowMainThreadQueries()
.build()
todoDao = db.todoDao()
}
@After
@Throws(IOException::class)
fun deleteDb() {
db.close()
}
@Test
@Throws(Exception::class)
fun insertAndGetTodo() = runBlocking {
val todoItem = TodoItem(itemId = 1, itemName = "Dummy Item", isDone = false)
todoDao.insert(todoItem)
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
- Click on Run TodoDatabaseTest to run the test
- If everything is fine, you should see
Top comments (0)