Android Native - How to use TypeConverter for Room

dimitrilc 2 Tallied Votes 2K Views Share

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:

  1. How to create TypeConverters to save reference types in a Room database.

Tools Required

  1. Android Studio. The version used in this tutorial is Bumblebee 2021.1.1 Patch 1.

Prerequisite Knowledge

  1. Basic Android.
  2. Basic Room.
  3. Basic Serialization.

Project Setup

To follow along with the tutorial, perform the steps below:

  1. Create a new Android project with the default Empty Activity.

  2. 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'
  3. In the same file, add the kapt annotation processor inside the plugins {} block.

     id 'org.jetbrains.kotlin.kapt'
  4. 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"
        }
     }
  5. 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
     )
  6. 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)
     }
  7. Create a new file called Grade.kt and add the code below.

     enum class Grade {
        JUNIOR, SOPHOMORE, SENIOR
     }
  8. 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
     }
  9. 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.

1.jpg

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)
}

2.jpg

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.

  1. Add the top level variable below into MainActivity.kt.

     private const val TAG = "MAIN_ACTIVITY"
  2. 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.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.