Room and Custom Types

So far, all of our properties have been basic primitives (such as Int) or String. There is a good reason for that: those are all that Room understands “out of the box”. Everything else requires some amount of assistance on our part.

Sometimes, a property in an entity will be related to another entity. Those are relations, and we will consider those in the next chapter.

However, other times, a property in an entity does not map directly to primitives and String types, or to another entity. For example:

And so on.

In this chapter, we will explore two approaches for handling these things without creating another entity class: type converters and embedded types.

Type Converters

Type converters are a pair of functions, annotated with @TypeConverter, that map the type for a single database column to a type for a Kotlin property. So, for example, we can:

However, type converters offer only a 1:1 conversion: a single property to and from a single SQLite column. If you have a single property that should map to several SQLite columns, the @Embedded approach can handle that, as we will see later in this chapter.

Setting Up a Type Converter

First, define a Kotlin class somewhere. The name, package, superclass, etc. do not matter.

Next, for each type to be converted, create two functions that convert from one type to the other. So for example, you would have one function that takes an Instant and returns a Long (e.g., returning the milliseconds-since-the-Unix-epoch value), and a counterpart function that takes a Long and returns an Instant. If the converter function is passed null, the proper result is null. Otherwise, the conversion is whatever you want, so long as the “round trip” works, so that the output of one converter function, supplied as input to the other converter function, returns the original value.

Then, each of those functions get the @TypeConverter annotation. The function names do not matter, so pick a convention that works for you.

Finally, you add a @TypeConverters annotation, listing this and any other type converter classes, to… something. What the “something” is controls the scope of where that type converter can be used.

The simple solution is to add @TypeConverters to the RoomDatabase, which means that anything associated with that database can use those type converters. However, sometimes, you may have situations where you want different conversions between the same pair of types, for whatever reason. In that case, you can put the @TypeConverters annotations on narrower scopes:

@TypeConverters Location Affected Areas
Entity class all properties in the entity
Entity property that one property in the entity
DAO class all functions in the DAO
DAO function that one function in the DAO, for all parameters
DAO function parameter that one parameter on that one function

For example, the TransmogrifyingEntity file in the the MiscSamples module of the book’s primary sample project has not only TransmogrifyingEntity but also a TypeTransmogrifier class. A transmogrifier is a ~30-year-old piece of advanced technology that can convert one thing into another. TypeTransmogrifier has a set of functions that turn one type into another — we will examine those functions in upcoming sections. TransmogrifyingEntity itself has the @TypeConverters annotation, indicating that the type converters on TypeTransmogrifier should be used for that entity:

@Entity(tableName = "transmogrified")
@TypeConverters(TypeTransmogrifier::class)
data class TransmogrifyingEntity(
  @PrimaryKey(autoGenerate = true)
  val id: Long = 0,
  val creationTime: Instant = Instant.now(),
  val location: Location = Location(null as String?).apply {
    latitude = 0.0
    longitude = 0.0
  },
  val tags: Set<String> = setOf()
) {
  @Dao
  interface Store {
    @Query("SELECT * FROM transmogrified")
    fun loadAll(): List<TransmogrifyingEntity>

    @Insert
    fun insert(entity: TransmogrifyingEntity)
  }
}

Example: Dates and Times

A typical way of storing a date/time value in a database is to use the number of milliseconds since the Unix epoch (i.e., the number of milliseconds since midnight, 1 January 1970). Instant has a getEpochMillis() function that returns this value.

The TypeTransmogrifier class has a pair of functions designed to convert between an Instant and a Long:

  @TypeConverter
  fun instantToLong(timestamp: Instant?) = timestamp?.toEpochMilli()

  @TypeConverter
  fun longToInstant(timestamp: Long?) =
    timestamp?.let { Instant.ofEpochMilli(it) }

Each has the @TypeConverter annotation, so Room knows to examine those functions when it has the need to convert between types, such as finding some Room-native type into which we can convert an Instant.

TransmogrifyingEntity has an Instant property named creationTime:

  val creationTime: Instant = Instant.now(),

Given that TransmogrifyingEntity has the @TypeConverters annotation pointing to TypeTransmogrifier, Room will be able to find a way to convert that Instant to something that it knows how to handle: a Long. As a result, our timestamp will be stored in a SQLite INTEGER column.

Example: Locations

A Location object contains a latitude, longitude, and perhaps other values (e.g., altitude). If we only care about the latitude and longitude, we could save those in the database in a single TEXT column, so long as we can determine a good format to use for that string. One possibility is to have the two values separated by a semicolon.

That is what these two type converter functions on TypeTransmogrifier do:

  @TypeConverter
  fun locationToString(location: Location?) =
    location?.let { "${it.latitude};${it.longitude}" }

  @TypeConverter
  fun stringToLocation(location: String?) = location?.let {
    val pieces = location.split(';')

    if (pieces.size == 2) {
      try {
        Location(null as String?).apply {
          latitude = pieces[0].toDouble()
          longitude = pieces[1].toDouble()
        }
      } catch (e: Exception) {
        null
      }
    } else {
      null
    }
  }

Our entity class has a Location property:

  val location: Location = Location(null as String?).apply {
    latitude = 0.0
    longitude = 0.0
  },

Room will know how to convert a Location to and from a String, so our location will be stored in a SQLite TEXT column.

However, the downside of using this approach is that we cannot readily search based on location. If your location data is not a searchable property, and it merely needs to be available when you load your entities from the database, using a type converter like this is fine. Later in this chapter, we will see another approach (@Embedded) that allows us to store the latitude and longitude as separate columns while still mapping them to a single data class in Kotlin.

Example: Simple Collections

TEXT and BLOB columns are very flexible. So long as you can marshal your data into a String or byte array, you can save that data in TEXT and BLOB columns. As with the comma-separated values approach in the preceding section, though, columns used this way are poor for searching.

So, suppose that you have a Set of String values that you want to store, perhaps representing tags to associate with an entity. One approach is to have a separate Tag entity and set up a relation. This is the best approach in many cases. But, perhaps you do not want to do that for some reason.

You can use a type converter, but you need to decide how to represent your data in a column. If you are certain that the tags will not contain some specific character (e.g., a comma), you can use the delimited-list approach demonstrated with locations in the preceding section. If you need more flexibility than that, you can always use JSON encoding, as these type converters do:

  @TypeConverter
  fun stringSetToString(list: Set<String>?) = list?.let {
    val sw = StringWriter()
    val json = JsonWriter(sw)

    json.beginArray()
    list.forEach { json.value(it) }
    json.endArray()
    json.close()

    sw.buffer.toString()
  }

  @TypeConverter
  fun stringToStringSet(stringified: String?) = stringified?.let {
    val json = JsonReader(StringReader(it))
    val result = mutableSetOf<String>()

    json.beginArray()

    while (json.hasNext()) {
      result += json.nextString()
    }

    json.endArray()

    result.toSet()
  }

Here, we use the JsonReader and JsonWriter classes that have been part of Android since API Level 11. Alternatively, you could use a third-party JSON library (e.g., Gson, Moshi).

Given these type conversion functions, we can use a Set of String values in TransmogrifyingEntity:

  val tags: Set<String> = setOf()

…where the tags will be stored in a TEXT column.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.