Room with Flow returns null when empty
Asked Answered
L

3

18

I've just started looking at Room, Coroutines, and Flow, and have come across something odd: what I'm expecting to be an empty flow actually has one null item in it.

My setup is as follows, with generic T for my actual entities.

interface TDao {

    @Query("SELECT * FROM Table WHERE date=:date")
    fun getT(date: String): Flow<T>
}
@Singleton
class TRepository @Inject constructor(
    private val apiService: TApiService,
    private val Tdao: TDao
) {

    suspend fun getTFor(date: String): Flow<T> =
        Tdao
            .getT(date)
            .map {
                if (it == null) {
                    returnTFromDatabase()
                } else {
                    it
                }
            }

Now, when the database doesn't have any T in it for date, I'm expecting it to return an empty flow, with no items in it. Instead, it has one null element, which should never happen, because T isn't nullable.

I wrote this test for it:

@RunWith(AndroidJUnit4::class)
class TDatabaseTest {

    private lateinit var db: TDatabase
    private lateinit var underTest: TDao

    @Before
    fun setUp() {
        val context = InstrumentationRegistry.getInstrumentation().context
        db = Room.inMemoryDatabaseBuilder(context, TDatabase::class.java).build()
        underTest = db.TDao()
    }

    @After
    fun tearDown() {
        db.close()
    }

    @Test
    fun givenEmptyDatabase_thenHasNoItems() {
        runBlocking {
            val list = underTest.getT("1999").take(1).toList()
            assertEquals(1, list.size)
        }
    }
}

...and it passes, cause, again, there's one null item returned.

What am I missing? What's wrong here, because I can't quite figure it out. Why am I getting a single null element in a flow with non nullable elements?

Lindon answered 5/3, 2020 at 18:13 Comment(3)
Room doesn't play nice with generic last time I've checked. Also, I recommend you having a read on Kotlin Generics. As the T doesn't have an upper bound the default upper bound is Any? (link)Duren
It's not generic in my code, I've just replaced mentions of my actual entities with a mention of T. It's an actual entity in my code.Lindon
@GiorgosNeokleous A given T may be non-nullable, therefore it is never ok to pass a null as a T < Any?. You just have to remember that T < T?.Knacker
P
25

How Room handles nullability depends on how you define the return type of the query function. The Docs say:

  • When the return type is Flow<T>, querying an empty table throws a null pointer exception.
  • When the return type is Flow<T?>, querying an empty table emits a null value.
  • When the return type is Flow<List<T>>, querying an empty table emits an empty list.

The above snippet discusses empty tables, but I assume the same behaviour is applicable to any query that returns no rows.

Source: Room Docs on Query

Petcock answered 16/9, 2020 at 20:28 Comment(2)
The docs say it throws a null pointer exception, but in practice I received a null value in my flow as well.Elswick
There discrepancy has been reported: Room Coroutine and Flow @Query do not handle null as expected. Looks to still be open, as of this writing. Solutions given is to switch to KSP or declare queries to return Flow<T?> and filter if desired (as Pierluigi suggests in his answer).Diathesis
T
24

Room is a db written in Java and that's why it ignores Kotlin optional. I suggest to declare always query return type or, in your case, Flow<T?> type, optional. If you don't want a null type in the flow you can use filterNotNull() function like this:

Tdao.getT(date).filterNotNull()
Tomikotomkiel answered 7/3, 2020 at 10:13 Comment(0)
T
1

One way to achieve proper nullability with Flow<T> that worked for me:

  • Use Room 2.6.0 or above
  • Use KSP instead of KAPT
  • In your app's build.gradle:
ksp {
    // Configure Room to generate Kotlin source code
    // This achieves proper nullability in Flow<T>
    arg("room.generateKotlin", "true")
}

There is also Room Gradle Plugin which can be used to configure this KSP option, but for me it didn't work (there seems to be some repository issue and plugin cannot be found).

Trichromat answered 12/3, 2024 at 14:38 Comment(1)
I tried this and now I am getting this error instead, which is more clear but I am not sure how it helps the situation. java.lang.IllegalStateException: The query result was empty, but expected a single row to return a NON-NULL object of type <net.martinlundberg.a1repmaxtracker.`data`.database.model.OneRMEntity>.Continental

© 2022 - 2025 — McMap. All rights reserved.