3

After updating Android Room from 2.2.6 to 2.3.0 it stopped processing @androidx.annotation.NonNull annotations, they are ignored now.

When installing the app after upgrade, I get:

Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.

Example code that used to work in Room 2.2.6:

@NonNull
@ColumnInfo(name = "span_type")
var spanType: SpanType? = null

after upgrading to 2.3.0 generates changes in schema (I have both exportSchema and room.schemaLocation set):

enter image description here

In the example above SpanType is enum, but I also have these annotations on other fields, e.g. @NonNull var objectId: Long? = null and they also get ignored after upgrade.

I know I can use workarounds but this is not what I want to do - I searched through docs, release notes, etc. and couldn't find any info that @NonNull support is deprecated or removed. I also tested with previous kotlin version (1.4.x instead of 1.5.20) and there's no difference.

The closest SO question is probably this one, but accepted answer is obviously wrong - kotlin supports @NonNull annotations.

Do you have any ideas what I am missing?

dominik
  • 2,404
  • 2
  • 29
  • 33
  • 1
    That answer you linked is correct. The nullability annotations are only intended for Java code because in Kotlin the nullability is indicated by whether or not you put a `?` after the type. It is contradictory that you are using `@NonNull` with a nullable property. It’s possible previous versions of Room accidentally checked for the annotation even in Kotlin code and that has been fixed now. – Tenfour04 Jul 09 '21 at 21:08
  • Additionally to what Tenfour04 says, check https://stackoverflow.com/questions/44411928/how-to-annotate-column-as-not-null-using-android-room-persistence-library/44413561 for a list of supported Room annotations. It could be that even though the upgrade to 2.3 was technically a minor update, it might've had a breaking change. – ByteWelder Jul 10 '21 at 11:31

1 Answers1

2

Ok, so according to the comments (thanks @Tenfour04 and @Ken Van Hoeylandt), there's no way to use @NonNull anymore.

Why this was problem for me

It seems that Room just disabled one of the security layers in enterprise apps - database constraints. It happened few times that my app crashed due to null check constraints and that was correct (concurrency issues where multiple threads operate on the same object). Now, when @NonNull doesn't work, instead of a crash, a database would get filled with junk data like id=-1 because there's no way to set initial null value for fields holding primitive types, e.g. @ColumnInfo(name = "object_id") lateinit var objectId: Long.

My workaround

  • For non-primitive types workaround is quite easy - I just replaced this:
@NonNull
@ColumnInfo(name = "span_type")
var spanType: SpanType? = null

with this:

@ColumnInfo(name = "span_type")
lateinit var spanType: SpanType

Schema generated by Room doesn't change, all null checks like if (spanType == null) {...} can be replaced with if (!::spanType.isInitialized) {...}

  • For primitive types workaround is little more complex

Given field:

@NonNull
@ColumnInfo(name = "object_id")
var objectId: Long? = null

cannot be converted to:

@ColumnInfo(name = "object_id")
lateinit var objectId: Long

The first option is to replace Long with BigInteger:

@ColumnInfo(name = "object_id")
lateinit var objectId: BigInteger

and write a @TypeConverter that converts BigInteger to Long. This works but BigInteger's in-memory size is significant (see this SO) and not ideal for Android.

Instead of using BigIntegers, I wrote simplier, lightweight wrapper for primitive types:

class LateinitPrimitive<T>(t: T?) where T : Number {
    var value: T? = t
}

so now I can declare:

@ColumnInfo(name = "object_id")
lateinit var objectId: LateinitPrimitive<Long>

Adding necessary converters:

@TypeConverter
fun lateinitPrimitiveToLong(lateinitPrimitive: LateinitPrimitive<Long>): Long? = lateinitPrimitive.value

@TypeConverter
fun lateinitPrimitiveFromLong(value: Long?): LateinitPrimitive<Long>? = value?.let { LateinitPrimitive(value) }

I achieved the same functionality that I had prior to Room 2.3.0 - if objectId is missing I get plain old android.database.sqlite.SQLiteConstraintException: NOT NULL constraint failed.

dominik
  • 2,404
  • 2
  • 29
  • 33