Introduction
When working with a Room database, we are mostly restricted to save data using primitives (and boxed primitives). Reference types are not supported right out of the box, but can be enabled by creating additional TypeConverter. If you are familiar with ORM-light frameworks such as Spring JDBC, then you can think of TypeConverters as being conceptually similar to RowMapper.
In this tutorial, we will learn how to use TypeConverters in a Room database. Existing basic knowledge of Room is required for this tutorial.
Goals
At the end of the tutorial, you would have learned:
- How to create TypeConverters to save reference types in a Room database.
Tools Required
- Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 1.
Prerequisite Knowledge
- Basic Android.
- Basic Room.
- Basic Serialization.
Project Setup
To follow along with the tutorial, perform the steps below:
-
Create a new Android project with the default Empty Activity.
-
Add the dependencies below into your Module build.gradle file inside the
dependencies {}
block.def roomVersion = "2.4.1" implementation "androidx.room:room-runtime:$roomVersion" annotationProcessor "androidx.room:room-compiler:$roomVersion" //To use Kotlin annotation processing tool (kapt) kapt "androidx.room:room-compiler:$roomVersion" //Kotlin Extensions and Coroutines support for Room implementation "androidx.room:room-ktx:$roomVersion" //lifecycle scope implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
-
In the same file, add the
kapt
annotation processor inside theplugins {}
block.id 'org.jetbrains.kotlin.kapt'
-
Create a new file called Teacher.kt and add the code below.
data class Teacher( val name: String, val age: Int ) { override fun toString(): String { return "$name:$age" } }
-
Create a new file called Classroom.kt and add the code below.
import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @Entity data class Classroom( @PrimaryKey(autoGenerate = true) val uid: Int = 0, val grade: Grade, @ColumnInfo(name = "homeroom_teacher") val homeroomTeacher: Teacher )
-
Create a new file called ClassroomDao.kt and add the code below.
import androidx.room.* @Dao interface ClassroomDao { @Query("SELECT * FROM classroom") fun getAll(): List<Classroom> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(vararg classrooms: Classroom) }
-
Create a new file called Grade.kt and add the code below.
enum class Grade { JUNIOR, SOPHOMORE, SENIOR }
-
Create a new file called SchoolDatabase.kt and add the code below.
import androidx.room.BuiltInTypeConverters import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters @Database(entities = [Classroom::class], version = 1) //@TypeConverters( // Converters::class, // builtInTypeConverters = BuiltInTypeConverters( // enums = BuiltInTypeConverters.State.DISABLED // ) //) abstract class SchoolDatabase : RoomDatabase() { abstract fun classroomDao(): ClassroomDao }
-
Create a new file called Converters.kt and add the code below.
package com.codelab.daniwebandroidroomdatabaseconverter import androidx.room.TypeConverter class Converters { @TypeConverter fun teacherToString(teacher: Teacher) = "$teacher" //Other options are json string, serialized blob @TypeConverter fun stringToTeacher(value: String): Teacher { val name = value.substringBefore(':') val age = value.substringAfter(':').toInt() return Teacher(name, age) } }
Project Overview
Our skeleton project for this tutorial is quite simple. We completely ignore the UI, ViewModel or Hilt DI to focus on the database here. The Room database SchoolDatabase includes a single table that stores classroom information. Each classroom contains an id, the Grade, and a Teacher.
Because Teacher is a reference type, so Room will refuse to compile for now. Upon compiling, we will receive the error messages below.
error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private final com.codelab.daniwebandroidroomdatabaseconverter.Teacher homeroomTeacher = null;
error: Cannot figure out how to read this field from a cursor.
private final com.codelab.daniwebandroidroomdatabaseconverter.Teacher homeroomTeacher = null;
These errors occur because we have not registered any TypeConverter to our SchoolDatabase.
Creating Converters
To help Room understand how to convert these reference types, we will have to create TypeConverters. TypeConverters can only be used to convert a column. In the the picture below, a TypeConverter pair can be used to convert a column into a primitive that Room allows, and vice versa.
TypeConverters are just functions annotated with @TypeConverter. We have already created some TypeConverters in the file Converters.kt, so let us inspect some of them.
The first function that we are going to look at is teacherToString()
.
@TypeConverter
fun teacherToString(teacher: Teacher) = "$teacher" //Other options are json string, serialized blob
In teacherToString()
, the only three things that really matter for Room are the parameter type, the return type, and the @TypeConverter
annotation. The compiler uses information from them to determine whether Room can properly use them to convert from one type to another. The parameter type and the return type are reversed when it comes to stringToTeacher()
.
@TypeConverter
fun stringToTeacher(value: String): Teacher {
val name = value.substringBefore(':')
val age = value.substringAfter(':').toInt()
return Teacher(name, age)
}
I have decided to store a string representation of a Teacher object in this tutorial because it is quick and simple. I have also overridden Teacher’s toString()
to make the deserialization easier.
override fun toString(): String {
return "$name:$age"
}
In real code, you can store your object in other ways, such as a serialized BLOB or a JSON string, with proper sanitization.
Pre-made TypeConverters
You might have noticed that I have not discussed the Grade enum at all. It is, after all, also a reference type. The simple reason why we do not have to provide TypeConverters for Grade is because Android already includes some premade TypeConverters from BuiltInTypeConverters.
Enums and UUID types are supported by default. The default support for enum uses the name()
value. If that is not good enough for you, then you can also provide custom TypeConverters for Enum and UUID. Your custom TypeConverters take precedence over the builtin ones.
Register the Converters with Room
The next step that we would need to do is to register the Converters with the database. You can do that by applying an @Converters annotation to the RoomDatabase class. Note that @Converter and @Converters are different annotations. We already have the @TypeConverters
annotation set up in SchoolDatabase.kt, but commented out. Uncomment it, and we will have the code below.
@TypeConverters(
Converters::class,
builtInTypeConverters = BuiltInTypeConverters(
enums = BuiltInTypeConverters.State.DISABLED
)
)
The builtInTypeConverters
argument is entirely optional. If you do not provide it any value, then it will enable the default TypeConverters. If we run the App now, we will receive a compile error of:
error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private final com.codelab.daniwebandroidroomdatabaseconverter.Grade grade = null;
This is because we have told Room to disable the builtin TypeConverter for enums. We also did not provide any custom enum TypeConverter. Simply comment out the builtInTypeConverters
argument for the code to compile.
@TypeConverters(
Converters::class
/* builtInTypeConverters = BuiltInTypeConverters(
enums = BuiltInTypeConverters.State.DISABLED
)*/
)
The Class object that we provided indicates that this class contains the @TypeConverter
functions, so Room should look there for any type that it does not know how to convert.
Run the App
The app should compile correctly now, but it does not do anything useful yet.
-
Add the top level variable below into MainActivity.kt.
private const val TAG = "MAIN_ACTIVITY"
-
Append the code below to
MainActivity#onCreate()
.val db = Room.databaseBuilder( applicationContext, SchoolDatabase::class.java, "school-db" ).build() lifecycleScope.launch(Dispatchers.IO) { val classroomDao = db.classroomDao() val teacher1 = Teacher( name = "Mary", age = 35 ) val teacher2 = Teacher( name = "John", age = 28 ) val teacher3 = Teacher( name = "Diana", age = 46 ) val classroom1 = Classroom( grade = Grade.JUNIOR, homeroomTeacher = teacher1 ) val classroom2 = Classroom( grade = Grade.SOPHOMORE, homeroomTeacher = teacher2 ) val classroom3 = Classroom( grade = Grade.SENIOR, homeroomTeacher = teacher3 ) classroomDao.insertAll( classroom1, classroom2, classroom3 ) val classrooms = classroomDao.getAll() classrooms.forEach { Log.d(TAG, "$it") } db.clearAllTables() }
Run the app now. We should be able to see the output below in Logcat.
2022-02-14 13:59:18.700 12504-12551/com.codelab.daniwebandroidroomdatabaseconverter D/MAIN_ACTIVITY: Classroom(uid=1, grade=JUNIOR, homeroomTeacher=Mary:35)
2022-02-14 13:59:18.700 12504-12551/com.codelab.daniwebandroidroomdatabaseconverter D/MAIN_ACTIVITY: Classroom(uid=2, grade=SOPHOMORE, homeroomTeacher=John:28)
2022-02-14 13:59:18.701 12504-12551/com.codelab.daniwebandroidroomdatabaseconverter D/MAIN_ACTIVITY: Classroom(uid=3, grade=SENIOR, homeroomTeacher=Diana:46)
Solution Code
MainActivity.kt
package com.codelab.daniwebandroidroomdatabaseconverter
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.lifecycleScope
import androidx.room.Room
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
private const val TAG = "MAIN_ACTIVITY"
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val db = Room.databaseBuilder(
applicationContext,
SchoolDatabase::class.java, "school-db"
).build()
lifecycleScope.launch(Dispatchers.IO) {
val classroomDao = db.classroomDao()
val teacher1 = Teacher(
name = "Mary",
age = 35
)
val teacher2 = Teacher(
name = "John",
age = 28
)
val teacher3 = Teacher(
name = "Diana",
age = 46
)
val classroom1 = Classroom(
grade = Grade.JUNIOR,
homeroomTeacher = teacher1
)
val classroom2 = Classroom(
grade = Grade.SOPHOMORE,
homeroomTeacher = teacher2
)
val classroom3 = Classroom(
grade = Grade.SENIOR,
homeroomTeacher = teacher3
)
classroomDao.insertAll(
classroom1,
classroom2,
classroom3
)
val classrooms = classroomDao.getAll()
classrooms.forEach {
Log.d(TAG, "$it")
}
db.clearAllTables()
}
}
}
Classroom.kt
package com.codelab.daniwebandroidroomdatabaseconverter
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Classroom(
@PrimaryKey(autoGenerate = true) val uid: Int = 0,
val grade: Grade,
@ColumnInfo(name = "homeroom_teacher") val homeroomTeacher: Teacher
)
ClassroomDao
package com.codelab.daniwebandroidroomdatabaseconverter
import androidx.room.*
@Dao
interface ClassroomDao {
@Query("SELECT * FROM classroom")
fun getAll(): List<Classroom>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(vararg classrooms: Classroom)
}
Converters.kt
package com.codelab.daniwebandroidroomdatabaseconverter
import androidx.room.TypeConverter
class Converters {
@TypeConverter
fun teacherToString(teacher: Teacher) = "$teacher" //Other options are json string, serialized blob
@TypeConverter
fun stringToTeacher(value: String): Teacher {
val name = value.substringBefore(':')
val age = value.substringAfter(':').toInt()
return Teacher(name, age)
}
}
Grade.kt
package com.codelab.daniwebandroidroomdatabaseconverter
enum class Grade {
JUNIOR, SOPHOMORE, SENIOR
}
**SchoolDatabase.kt**
package com.codelab.daniwebandroidroomdatabaseconverter
import androidx.room.BuiltInTypeConverters
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(entities = [Classroom::class], version = 1)
@TypeConverters(
Converters::class
/* builtInTypeConverters = BuiltInTypeConverters(
enums = BuiltInTypeConverters.State.DISABLED
)*/
)
abstract class SchoolDatabase : RoomDatabase() {
abstract fun classroomDao(): ClassroomDao
}
Teacher.kt
package com.codelab.daniwebandroidroomdatabaseconverter
data class Teacher(
val name: String,
val age: Int
) {
override fun toString(): String {
return "$name:$age"
}
}
Module **build.gradle**
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.kapt'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.codelab.daniwebandroidroomdatabaseconverter"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
def roomVersion = "2.4.1"
implementation "androidx.room:room-runtime:$roomVersion"
annotationProcessor "androidx.room:room-compiler:$roomVersion"
//To use Kotlin annotation processing tool (kapt)
kapt "androidx.room:room-compiler:$roomVersion"
//Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$roomVersion"
//lifecycle scope
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
Summary
We have learned how to use TypeConverters in a Room database. The full project code can be found at https://github.com/dmitrilc/DaniwebAndroidRoomDatabaseConverter.