What’s New in Room?

Room keeps changing, adding in new features with every major and minor release. This chapter highlights some of those changes.

Version 2.3.x

April 2021 saw the release of 2.3.0. As of late October 2021, no new patch releases have been required.

2.3.0 gave us a bunch of improvements, particularly around type and column management.

Enum Support

Historically, if your @Entity had a property that was some enum class, you needed to write a type converter to convert that enum to and from something else, such as an Int or String. That is still an option.

However, Room 2.3.0 added automatic conversion of an enum class value to and from its String representation. So, now you can use an enum without a @TypeConverter pair:

package com.commonsware.room.misc

import androidx.room.*

enum class Silly { First, Second, Third }

@Entity(tableName = "autoEnum")
data class AutoEnumEntity(
  @PrimaryKey(autoGenerate = true)
  var id: Long,
  var silly: Silly
) {
  @Dao
  abstract class Store {
    @Query("SELECT * FROM autoEnum")
    abstract fun loadAll(): List<AutoEnumEntity>

    @Query("SELECT * FROM autoEnum WHERE id = :id")
    abstract fun findById(id: Int): AutoEnumEntity

    fun insert(entity: AutoEnumEntity): AutoEnumEntity {
      entity.id = _insert(entity)

      return entity
    }

    @Insert
    abstract fun _insert(entity: AutoEnumEntity): Long
  }
}

In this case, our silly column is declared as TEXT in the generated SQL:

CREATE TABLE IF NOT EXISTS `autoEnum` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `silly` TEXT NOT NULL)

Part of what Room code-generates for us are functions to convert the enum class values to String representations and back:

  private String __Silly_enumToString(final Silly _value) {
    if (_value == null) {
      return null;
    } switch (_value) {
      case First: return "First";
      case Second: return "Second";
      case Third: return "Third";
      default: throw new IllegalArgumentException("Can't convert enum to string, unknown enum value: " + _value);
    }
  }

  private Silly __Silly_stringToEnum(final String _value) {
    if (_value == null) {
      return null;
    } switch (_value) {
      case "First": return Silly.First;
      case "Second": return Silly.Second;
      case "Third": return Silly.Third;
      default: throw new IllegalArgumentException("Can't convert value to enum, unknown value: " + _value);
    }
  }

Room then uses those functions as if they were a custom @TypeConverter, to convert our enum class values to and from a String representation to put in the TEXT column.

Type Converter Improvements

The @TypeConverter annotation is a useful way to get Room to recognize types that it otherwise could not handle. However, @TypeConverter had been fairly inflexible: you had to implement the functions on a class, where Room would instantiate that class as needed.

Room 2.3.0 improves the flexibility in this area.

The simple improvement is that now you can have @TypeConverter annotations on an object, saving Room the need to instantiate the class.

The more complex improvement is in a new @ProvidedTypeConverter annotation. This is for cases where you are happy to have the @TypeConverter functions be on a class, but you want to be in charge of creating the object for that class. One popular case is where you want to use a dependency inversion framework (Dagger/Hilt, Koin, etc.) — for example, perhaps you are providing a JSON converter via DI and need to inject the converter into an object that will use that converter for @TypeConverter functions.

To make this work, you write a class to house your @TypeConverter functions as normal. However, now you can add a constructor on that class or otherwise configure it as you see fit. You do, however, also have to add @ProvidedTypeConverter as a class-level annotation:

@ProvidedTypeConverter
class SomeValueTypeConverter(private val adapter: JsonAdapter<SomeValueType>) {
  @TypeConverter
  fun someValueFromJson(json: String) = adapter.fromJson(json)

  @TypeConverter
  fun someValueToJson(value: SomeValueType) = adapter.toJson(value)
}

Here, we have a SomeValue type (not shown here) and an injected Moshi adapter for that type. The @TypeConverter-annotated functions simply delegate to Moshi.

@ProvidedTypeConverter tells Room to allow classes without a public zero-argument constructor. However, the cost is in the second step: you need to supply the instance of your @ProvidedTypeConverter class to the RoomDatabase.Builder, via an addTypeConverter() call:

class YourRepository(private val someValueTypeAdapter: JsonAdapter<SomeValueType>) {
  val db = Room.databaseBuilder(context, YourDatabase::class.java, DB_NAME)
    .addTypeConverter(SomeValueTypeConverter(someValueTypeAdapter))
    .build()
}

The name of the annotation explains the rule: by using @ProvidedTypeConverter, you have more flexibility, but you have the corresponding responsibility to provide the instance of the type converter to the RoomDatabase.

Packaged Databases Improvements

As we have seen, you can package a database with your app, so you have starter data immediately when the app is first opened. Room 2.3.0 makes a couple of improvements in this area.

In addition to using databases packaged as assets — as the chapter on packaged databases focuses on — you can use the same technique to populate your database from a file, using createFromFile() instead of createFromAsset(). This, however, requires a File object, and those are more difficult to come by given the “scoped storage” added to Android 10. Room 2.3.0 now gives us createFromInputStream(), so we can get our starter database from a InputStream, such as one that we might get from openInputStream() on a ContentResolver using a Uri.

Also, these create...() functions now have a variant that takes an additional RoomDatabase.PrepackagedDatabaseCallback object (whose name makes the author very grateful for auto-completion in modern IDEs…). This will be called with onOpenPrepackagedDatabase() after the data source has been copied and the Room database is set up. That way, if you need to do some cleanup (e.g., delete a downloaded file from getCacheDir()), you know it is safe to do so.

@RewriteQueriesToDropUnusedColumns

Using * in a @Query SQL SELECT statement is easy. However, by default, Room is not very sophisticated about using it. Room will happily retrieve all the columns available to it from your table, even if your entity or other output object only uses a few of those columns. Plus, you will get a compile-time warning from Room about the inefficiency. It is more efficient to have your SELECT list the exact columns that you need, but this gets tedious to keep in sync with your output object structures.

Adding @RewriteQueriesToDropUnusedColumns to your @Query-annotated function tells Room to try to handle this automatically. The Room compiler will replace your * with the column names that are needed in the SQL that it actually executes. That way, you get the convenience of * and the efficiency of only querying for the needed columns. You can also add @RewriteQueriesToDropUnusedColumns to the entire @Dao class or interface, to have it apply to all @Query functions.

Paging 3 Support

If you are using Paging 3 — the now-latest generation of the Jetpack Paging framework — Room will now let you set up DAO functions that support it. The chapter on paging has been updated to show Paging 3.

RoomDatabase.QueryCallback

For logging purposes, it might be useful to learn when Room executes some SQL. For that, you can call setQueryCallback() on your RoomDatabase.Builder when you are setting up Room. This function takes a RoomDatabase.QueryCallback object, which has a single onQuery() function — this makes it easy to be replaced by a lambda expression in Java or Kotlin. There, you get a String of the SQL to be executed and a List of the objects to be bound as replacements for ?-style placeholders in that SQL.

Do be careful, though, about your logging, as the bind objects may contain data that should be considered private. Consider only logging in debug builds.

RxJava 3 Support

Not everybody realizes this, but RxJava 2.x is end-of-life: other than some bug fixes, no new updates are planned for that. The RxJava team has moved on to RxJava 3, which has a similar API to RxJava 2.x but not quite identical.

Previously, Room supported other reactive options — LiveData, RxJava 2.x, coroutines — but not RxJava 3. Now, using androidx.room:room-rxjava3, you can use RxJava 3 types in your Room DAO functions, if you so choose.


Prev Table of Contents

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