This article will introduce how to use Room to access databases on Android.
The complete code for this chapter can be found in .
Table of Contents
Room
Room is a Database Object Mapping library to access databases. It makes the whole process of accessing databases very simple, and it also integrates Coroutine suspend function. In this article, we’ll build a simple app to show how to use Room.
First, create a new project. Then, in the project, add Room’s dependecies, as follows.
plugins { id 'kotlin-kapt' } dependencies { def room_version = "2.4.2" implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" }
@Entity
Each class that declares @Entity represents a table. In the following code, we declare class Employee and add @Entity on it. In @Entity, we specify the name of the table as employees
.
In this table, we declare 4 columns. We can use @ColumnInfo to specify the name of each column. If not specified, it uses the field name by default. Finally, we use @PrimaryKey to specify the primary key.
import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.util.* @Entity(tableName = "employees") data class Employee( val name: String, val type: Type, @ColumnInfo(name = "created_at") val createdAt: Date = Date(), @PrimaryKey(autoGenerate = true) var id: Int = 0, ) { enum class Type { FULL_TIME, PART_TIME, } }
@Dao
@Dao marks an interface or abstract class as Data Access Object (DAO). Data Access Object mainly defines the methods for accessing the database. As in the following code, we declare an interface EmployeeDao. Each method can be declared as a suspend function or a general method. Allows us to easily integrate with Coroutine.
CRUD:@Insert、@Update、@Delete、@Query
@Insert marks a method as an insert method. onConflict specifies the action to be taken when a conflict occurs. You can refer to all the actions in OnConflictStrategy.
@Update marks a method as an update method.
@Upsert marks a method as an insert or update method. It checks if the parameter already exists in the database. If it already exists in the database, it will update the entity.
@Delete marks a method as a delete method.
@Query marks a method as a query method. In @Query, we must write SQL statement and bind arguments. In @Query, not only SELECT, but also DELETE can be written. In addition, the returned value can also be Flow.
import androidx.room.* @Dao interface EmployeeDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(employee: Employee) @Update suspend fun update(employee: Employee) @Delete suspend fun delete(employee: Employee) @Query("SELECT * FROM employees") suspend fun findAll(): List<Employee> @Query("SELECT * FROM employees WHERE name = :name") suspend fun findByName(name: String): List<Employee> }
@Transaction
@Transaction marks a method as a transaction method. In the transaction method, all operations are performed in a transaction.
import androidx.room.* @Dao abstract class EmployeeDao { @Delete abstract suspend fun delete(employee: Employee) @Transaction suspend fun delete(list: List<Employee>) { list.forEach { delete(it) } } }
@RawQuery
@RawQuery marks a method that can execute the incoming SQL string. When we need to generate different SQL query strings according to different situations, we must generate SQL query strings by ourselves. Then, call the method marked with @RawQuery to execute our SQL query string.
import androidx.room.* import androidx.sqlite.db.SimpleSQLiteQuery @Dao abstract class EmployeeDao { @RawQuery abstract suspend fun execSelect(query: SimpleSQLiteQuery): List<Employee> suspend fun findByNameOptional(name: String?): List<Employee> { var sql = "SELECT * FROM employees" name?.let { sql += " WHERE name = $it" } val query = SimpleSQLiteQuery(sql) return execSelect(query) } }
@Database
@Database marks a class as RoomDatabase. This class must be an abstract class and inherit RoomDatabase, as EmployeeDatabase
shown in the code below. In @Database, we use entities
to specify the table in this database.
In EmployeeDatabase
, we declare an abstract method called dao()
. We don’t need to implement dao()
as it will be generated automatically. After that, we’ll call this dao()
to get EmployeeDao
, and then execute the insert()
or findAll()
methods.
Then, we also declare a method called getInstance()
to initialize an EmployeeDatabase object. When we call any method inside EmployeeDao, it must happen in a non-UI thread. Otherwise, an exception will be thrown. However, if you want to call any method inside EmployeeDao, you need to call .allowMainThreadQueries()
when create EmployeeDatabase .
Additionally, we declare getTestingInstance()
method.
The difference from getInstance() is that it initializes an in-memory database, not a database file. This is quite handy during unit testing, since we don’t need to clean up the database files at the end.
import android.content.Context import androidx.annotation.VisibleForTesting import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters @Database(entities = [Employee::class], version = 1) @TypeConverters(Converters::class) abstract class EmployeeDatabase : RoomDatabase() { abstract fun dao(): EmployeeDao companion object { @Volatile private var INSTANCE: EmployeeDatabase? = null fun getInstance(context: Context, path: String): EmployeeDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, EmployeeDatabase::class.java, path, ) // .allowMainThreadQueries() .build() INSTANCE = instance instance } } @VisibleForTesting @Synchronized fun getTestingInstance(context: Context): EmployeeDatabase { return Room .inMemoryDatabaseBuilder(context.applicationContext, EmployeeDatabase::class.java) .build() } } }
@TypeConverters
In the above code, there is an annotation called @TypeConverters. When Room is about to write Employee
to the database, or read data from the database and convert it to Employee, Room only knows how to convert primitive types. Let’s recall class Employee, its type
and createdAt
are both non-primitive types. So, we have to tell Room how to convert these non-primitive type data into primitive type.
@Entity(tableName = "employees") data class Employee( val name: String, val type: Type, @ColumnInfo(name = "created_at") val createdAt: Date = Date(), @PrimaryKey(autoGenerate = true) var id: Int = 0, ) { enum class Type { FULL_TIME, PART_TIME, } }
We declare 4 methods in class Converters. fromDate()
is to tell Room how to convert Date to a primitive type. Here we choose to convert Date to Long, which is Date’s timestamp. On the other hand, toDate()
is to tell Room how to convert Long to Date. Finally, fromType()
and toType()
are to tell Room how to convert between enum Employee.Type and String.
Annotation @TypeConverter should be added to each method. Then, specify class Converters for EmployeeDatabase with @TypeConverters.
import androidx.room.TypeConverter import java.util.* class Converters { @TypeConverter fun fromDate(value: Date) = value.time @TypeConverter fun toDate(value: Long) = Date(value) @TypeConverter fun fromType(value: Employee.Type) = value.name @TypeConverter fun toType(value: String) = Employee.Type.valueOf(value) }
Debugging
When we are debugging, we may want to know the SQL statement that was executed. We can output the SQL statement executed each time with setQueryCallback()
.
@Database(entities = [Employee::class], version = 1) @TypeConverters(Converters::class) abstract class EmployeeDatabase : RoomDatabase() { abstract fun dao(): EmployeeDao companion object { fun getInstance(context: Context, path: String): EmployeeDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, EmployeeDatabase::class.java, path, ) .setQueryCallback({ sqlQuery, bindArgs -> println("SQL: $sqlQuery; Args: $bindArgs") }, Executors.newSingleThreadExecutor()) .build() INSTANCE = instance instance } } } }
Example
In this article, we implement a simple insertion and query app, you can download the complete code. The following only shows EmployeeListViewModel to demonstrate how to access a database in a ViewModel.
import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class EmployeeListViewModel : ViewModel() { val employees: MutableLiveData<List<Employee>> = MutableLiveData() private lateinit var database: EmployeeDatabase fun initDatabase(context: Context) { val path = context.getDatabasePath("employee") database = EmployeeDatabase.getInstance(context, path.absolutePath) } suspend fun addEmployee(name: String, type: Employee.Type) = withContext(Dispatchers.IO) { val employee = Employee(name, type) database.dao().insert(employee) val list = database.dao().findAll() employees.postValue(list) } suspend fun searchByName(name: String) = withContext(Dispatchers.IO) { val list = if (name.isEmpty()) database.dao().findAll() else database.dao().findByName(name) employees.postValue(list) } }
If you download this example and execute it, you can find the database file with Device File Explorer. Its path is /data/data/com.waynestalk.example/databases/, where there are 3 files, employee, employee-shm, and employee-wal. For each database, Room generates 3 files.
Conclusion
Room makes it easy to access databases, compared to ORMLite. Moreover, it also integrates suspend function.