5
votes

I am currently developing a new android app using Kotlin. I tried implementing Room for data storage, but I didn't get it to work with Kotlin delegates.

I created an Identifier delegate in order to ensure the id is not changed after initialization. The delegate looks like this:

class Identifier: ReadWriteProperty<Any?, Long> {

    private var currentValue = -1L

    override fun getValue(thisRef: Any?, property: KProperty<*>): Long {
        if (currentValue == -1L) throw IllegalStateException("${property.name} is not initialized.")
        return currentValue
    }

    override fun setValue(thisRef: Any?, property KProperty<*>, value: Long) {
        if (currentValue != -1L) throw IllegalStateException("${property.name} can not be changed.")
        currentValue = value
    }
}

My entity class looks like this:

@Entity
class Sample {

    @delegate:PrimaryKey(autoGenerate = true)
    var id by Identifier()
}

When I try to start the app, kapt gives me the following error message:

Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private final com.myapp.persistence.delegates.Identifier id$delegate = null;

Can I somehow get this to work without writing a TypeConverter for every delegate?

3

3 Answers

17
votes

Use @delegate:Ignore.

I had similar problem with my Entity Object and ... by lazy properties.

For example:

var name: String = "Alice"

val greeting: String by lazy { "Hi $name" }

The issue here is Room "cannot figure out how to save this field into database". I tried to add "@Ignore" but got a lint message saying "This annotation is not applicable to target 'member property with delegate'."

Turns out, the correct annotation in this case is @delegate:Ignore.

Solution:

var name: String = "Alice"

@delegate:Ignore
val greeting: String by lazy { "Hi $name" }
2
votes

Unfortunatelly, no - Room by default creates a column for each field that's defined in the entity and when we use delegate we get generated code like this:

   @PrimaryKey(autoGenerate = true)
   @NotNull
   private final Identifier id$delegate = new Identifier();

   public final long getId() {
      return this.id$delegate.getValue(this, $$delegatedProperties[0]);
   }

   public final void setId(long var1) {
      this.id$delegate.setValue(this, $$delegatedProperties[0], var1);
   }

and that's why Room tries to create column for Identifier id$delegate.

However, if you just want to ensure id is not changed after object initialization you don't need delegate at all, simply mark variable as final and place it in constructor eg:

@Entity
data class Sample(
    @PrimaryKey(autoGenerate = true)
    val id: Long
)
0
votes

I had a similar issue with the following code:

data class ForecastItem(
val city: String,
val time: Long,
val temp: Int,
val tempMax: Int,
val tempMin: Int,
val icon: String
) {
    val formattedTime: String by lazy {
        val date = Date(this.time * 1000L)
        val dateFormat = SimpleDateFormat("E HH:mm")
        dateFormat.timeZone = TimeZone.getTimeZone("GMT+1")
        dateFormat.format(date)
  }
}

In this case, I got the same error you got because of the delegation with formattedTime:

Cannot figure out how to save this field into database. You can consider adding a type converter for it.

In my case, I ended up replacing the delegate by a function. It is not the same, but it worked for me. I am not sure if this is actually the best way of designing the solution, but I hope it helps anyone with a similar problem.

fun getFormattedTime(): String {
    val date = Date(this.time * 1000L)
    val dateFormat = SimpleDateFormat("E HH:mm")
    dateFormat.timeZone = TimeZone.getTimeZone("GMT+1")
    return dateFormat.format(date)
}