Room database with Kotlin inline class as an Entity field

1.3k Views Asked by At

I am trying to get Room(https://developer.android.com/topic/libraries/architecture/room) work with Kotlin's inline classes as described in Jake Whartons article Inline Classes Make Great Database IDs:

@Entity
data class MyEntity(
    @PrimaryKey val id: ID,
    val title: String
)

inline class ID(val value: String)

When compiling this Room complains that

Entities and Pojos must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type).

Looking into the generated Java code I find:

private MyEntity(String id, String title) {
      this.id = id;
      this.title = title;
}

// $FF: synthetic method
public MyEntity(String id, String title, DefaultConstructorMarker $constructor_marker) {
      this(id, title);
}

Mysteriously the default constructor is private now.

When using String as a type for id (or a typealias), the generated Java class constructor looks like expected:

public MyEntity(@NotNull String id, @NotNull String title) {
  Intrinsics.checkParameterIsNotNull(id, "id");
  Intrinsics.checkParameterIsNotNull(title, "title");
  super();
  this.id = id;
  this.title = title;
}

Does somebody now how to keep the default constructor public while using Inline Classes as data entity properties?

3

There are 3 best solutions below

0
On

With the answer from Lyubomyr Ivanitskiy and some tinkering it can be done.

@Entity
class Test(
    @PrimaryKey(autoGenerate = true)
    @get:JvmName("getId")
    @set:JvmName("setId")
    var id: ID,
) {
    constructor(): this(ID(0)) // This is required as replacement for 
    constructor with actual fields
}

When trying to load this entity using a dao it will fail due to the getter method not being generated. It does not work for me using the inner class ID. So it needs to be tricked like this:

@Dao
interface TheDao {
    @Deprecated(
        "This is just for the generated Dao_Impl",
        level = DeprecationLevel.WARNING,
        replaceWith = ReplaceWith("getByIdRealId(theID)")
    )
    @Query("select * from test where id = :theID")
    fun getByIdLongType(theID: Long): Test

}

fun TheDao.getByIdRealId(theID: ID): Test = getByIdLongType(theID.id)

This will not prevent using the getById with Long parameter but generate at least a warning about it.

TestCode:

@Test
fun createAndLoadTest() {
    val toBeSaved = Test(ID(42))
    dao.save(toBeSaved)
    val fromDB = dao.getByIdRealId(ID(42))
    fromDB shouldNotBe null
    fromDB.id shouldNotBe 42
    fromDB.id shouldBe ID(42)
}
0
On

Kotlin inline classes use name mangling.

So I believe your Room database cannot find the getter and setter for you ID field. Try to add:

...
@get:JvmName("getID")
@set:JvmName("setID")
@PrimaryKey val id: ID,

before your ID parameter declaration to disable mangling. It helps to me

2
On

I believe the reason is that the ID class will be represented as String in runtime. So the $constructor_marker additional parameter is to guarantee the uniqueness of the MyEntity(String id, String title) constructor signature, cause this constructor could already have been defined. But I'm just speculating here.

Could you try to explicitly define this constructor in MyEntity class and see if it works?