How does Room know how to use a custom TypeConverter when the converter class doesn't implement any interface?

75 Views Asked by At

This is the way we create a database using the Room library:

database = Room.databaseBuilder(
        this,
        WordInfoDatabase::class.java,
        "word_db"
    ).addTypeConverter(Converters(GsonParser(Gson())))
        .build()

We can specify there what custom converter we want to use by the method addTypeConverter(typeConverter: Any). The typeConverter class' methods can have any names varying from toJson or fromJson to just qwerty, they can also take different data types as the parameter and return different data types as the result, e.g.

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

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

or

@ProvidedTypeConverter
class Converters(
    private val jsonParser: JsonParser
) {
    @TypeConverter
    fun fromMeaningsJson(json: String): List<Meaning> {
        return jsonParser.fromJson<ArrayList<Meaning>>(
            json,
            object : TypeToken<ArrayList<Meaning>>(){}.type
        ) ?: emptyList()
    }

    @TypeConverter
    fun toMeaningsJson(meanings: List<Meaning>): String {
        return jsonParser.toJson(
            meanings,
            object : TypeToken<ArrayList<Meaning>>(){}.type
        ) ?: "[]"
    }
}

So how does Room know how to use them properly?

1

There are 1 best solutions below

0
MikeT On

When you compile the project the Room annotations result in underlying java code being generated according to the room-compiler annotation processor and it is through invocation of the generated code that the Type Converters are used.

Consider a simplified version of your code, with some assumed additional classes:-

First a Meaning class (that will be converted):-

data class Meaning(
    val col1: String,
    val col2: String
)

And an associated pair of TypeConverter functions (annotated accordingly) for the Meaning class in the Converters class:-

class Converters  {
    @TypeConverter
    fun fromMeaningsJson(json: String): Meaning = Gson().fromJson(json,Meaning::class.java)
    @TypeConverter
    fun toMeaningsJson(meaning: Meaning): String = Gson().toJson(meaning)
}

An @Entity, class for a table that has a Meaning within a column that will require type conversion:-

@Entity
data class Table1(
    @PrimaryKey
    val id: Long?=null,
    val m1: Meaning
)

An @Dao annotated interface with 2 functions (Insert and Extract) that will require type conversion to take place

@Dao
interface AllDAOs {
    @Insert
    fun insert(table1: Table1): Long
    @Query("SELECT * FROM table1")
    fun getALlTable1Row(): List<Table1>
}

Finally an @Database annotated abstract class that is

  1. preceded by the @TypeConverters annotation (fullest scope of the TypeConverters)
  2. includes the respective @Entity annotated class
  3. has a function to retrieve the @Dao interface (so they are included)
  4. it is the combination of these annotations (a valid @Database with the respective @Entity's and the respective @Dao`s) that drives the generation of the generated java.

:-

@TypeConverters(Converters::class)
@Database(entities = [Table1::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDAOs(): AllDAOs
}

When the above is compiled then (via the Android View):-

enter image description here

So as can be seen there are two relevant java classes. One for the @Dao annotated interface (more if more @Dao annotated interfaces) with the same name but suffixed with _Impl. The other likewise for the @Database annotated class (more if more @Database annotated classes), again suffixed with _Impl.

It is from the the AllDAO_Impl class where you can see how the TypeConverter functions are implemented/called:-

@SuppressWarnings({"unchecked", "deprecation"})
public final class AllDAOs_Impl implements AllDAOs {
  private final RoomDatabase __db;

  private final EntityInsertionAdapter<Table1> __insertionAdapterOfTable1;

  private final Converters __converters = new Converters();

  public AllDAOs_Impl(@NonNull final RoomDatabase __db) {
    this.__db = __db;
    this.__insertionAdapterOfTable1 = new EntityInsertionAdapter<Table1>(__db) {
      @Override
      @NonNull
      protected String createQuery() {
        return "INSERT OR ABORT INTO `Table1` (`id`,`m1`) VALUES (?,?)";
      }

      @Override
      protected void bind(@NonNull final SupportSQLiteStatement statement,
          @NonNull final Table1 entity) {
        if (entity.getId() == null) {
          statement.bindNull(1);
        } else {
          statement.bindLong(1, entity.getId());
        }
        final String _tmp = __converters.toMeaningsJson(entity.getM1());
        if (_tmp == null) {
          statement.bindNull(2);
        } else {
          statement.bindString(2, _tmp);
        }
      }
    };
  }

  @Override
  public long insert(final Table1 table1) {
    __db.assertNotSuspendingTransaction();
    __db.beginTransaction();
    try {
      final long _result = __insertionAdapterOfTable1.insertAndReturnId(table1);
      __db.setTransactionSuccessful();
      return _result;
    } finally {
      __db.endTransaction();
    }
  }

  @Override
  public List<Table1> getALlTable1Row() {
    final String _sql = "SELECT * FROM table1";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    __db.assertNotSuspendingTransaction();
    final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
    try {
      final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
      final int _cursorIndexOfM1 = CursorUtil.getColumnIndexOrThrow(_cursor, "m1");
      final List<Table1> _result = new ArrayList<Table1>(_cursor.getCount());
      while (_cursor.moveToNext()) {
        final Table1 _item;
        final Long _tmpId;
        if (_cursor.isNull(_cursorIndexOfId)) {
          _tmpId = null;
        } else {
          _tmpId = _cursor.getLong(_cursorIndexOfId);
        }
        final Meaning _tmpM1;
        final String _tmp;
        if (_cursor.isNull(_cursorIndexOfM1)) {
          _tmp = null;
        } else {
          _tmp = _cursor.getString(_cursorIndexOfM1);
        }
        _tmpM1 = __converters.fromMeaningsJson(_tmp);
        _item = new Table1(_tmpId,_tmpM1);
        _result.add(_item);
      }
      return _result;
    } finally {
      _cursor.close();
      _statement.release();
    }
  }

  @NonNull
  public static List<Class<?>> getRequiredConverters() {
    return Collections.emptyList();
  }
}
  • Obviously a little different for provided type converters