androidkotlinandroid-jetpack-composeandroid-roomandroid-livedata

Room database not returning prepopulated data on first app run with Flow and LiveData in Jetpack Compose


I'm using a Room database in my Jetpack Compose app, and I'm prepopulating data into the database when it is created. However, when I try to fetch the data using a Flow or LiveData via a getAll() function in my DAO, I don't receive any data on the first app run.

Strangely, when I restart the app, the data is fetched and displayed as expected. It seems like the prepopulated data isn't available for the first query execution.

Could someone help me understand why the data isn't available initially and how to fix this issue?

Here's the relevant code:

@Dao
interface CategoryDAO {
    @Query("SELECT * FROM categories")
    fun getAll(): LiveData<List<CategoryEntity>>

    @Insert
    suspend fun insertCategory(category:CategoryEntity)

    @Insert
    fun insertAll(categories: List<CategoryEntity>)

    @Delete
    suspend fun deleteCategory(category: CategoryEntity)
}
interface CategoryRepository {
    fun getAll() : LiveData<List<Category>>
    suspend fun createCategory(category: Category)
    suspend fun deleteCategory()
}

In CategoryRepositoryImpl I just map data from CategoryEntity to Category:

class CategoryRepositoryImpl @Inject constructor(
    private val categoryDAO: CategoryDAO
) : CategoryRepository {

    override fun getAll(): LiveData<List<Category>> {
        return categoryDAO.getAll().map { entityList ->
            entityList.map { entity ->
                entity.toDomain()
            }
        }
    }
}

Using Hilt I instanciate Database

@Provides
    @Singleton
    fun provideCategoryDatabase(
        @ApplicationContext appContext:Context
    ) : CategoryDatabase {
        return Room.databaseBuilder(
            appContext,
            CategoryDatabase::class.java,
            "categories-db"
        )
            .addCallback(object : RoomDatabase.Callback() {
                override fun onCreate(db: SupportSQLiteDatabase) {
                    super.onCreate(db)
                    Executors.newSingleThreadExecutor().execute {
                        val dao = provideCategoryDatabase(appContext).CategoryDAO()
                        dao.insertAll(categories = prepopulateData())
                    }
                    Log.d("RoomDatabase", "Database created and prepopulated!")

                }
            })
            .build()
    }

prepopulateData() is a function that returns List

My ViewModel looks like this:

@HiltViewModel
class CategoryViewModel @Inject constructor(
    private val categoryRepository: CategoryRepository
):ViewModel(){

    val categories: LiveData<List<Category>> = categoryRepository.getAll()
}

And I use it in my CategoryDialog like this:

@Composable
fun CategoryDialog(
    showDialog: MutableState<Boolean>,
    categoryViewModel: CategoryViewModel = hiltViewModel()
){

    val categories by categoryViewModel.categories.observeAsState(emptyList())

   ... Rest of the UI 

    LazyVerticalGrid(
                    contentPadding = PaddingValues(20.dp),
                    columns = GridCells.Fixed(3),
                    horizontalArrangement = Arrangement.spacedBy(12.dp),
                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    items(categories){
                        CategoryItem(
                            category = it,
                            onClick = {
                               // TODO("ADD FUNCTIONALITY TO SELECT CATEGORY")
                            }
                        )
                    }
                }

}

Solution

  • The problem is that you have two different database objects, and changes done by one object is not automatically reflected in the other, although they are backed by the same database on the file system.

    One object is created when Hilt calls provideCategoryDatabase to satisfy the dependencies for CategoryRepositoryImpl (which needs a DAO which needs a Database). The other is created when this database's onCreate callback is executed. There, provideCategoryDatabase is called again, creating a second instance for the same database. You annotated the function with @Singleton, but that only has an effect when called by the dependency injection framework (i.e. Hilt), not when explicitly called like you just did.

    You use this second instance to add the categories, but the first instance isn't automatically notifyed and doesn't re-read the database from the file system, so you never the see changes. Only when you restart the app the database file is read again, and then the initial categories are displayed.

    The quick fix would be to provide the option enableMultiInstanceInvalidation() to the database builder. That allows the two database instances to communicate with each other, with the second one notifying the first one of the changes. It is quite inefficient, though.1

    It would be better not to have two different database instances in the first place. You have basically three alternatives:

    1. The onCreate callback already provides a database instance. It is a raw SupportSQLiteDatabase though, without the Room wrapper and without the DAO. You would need to call db.insert() yourself. See here for more: ROOM database: insert static data before other CRUD operations

      This is a dirty hack bypassing the Room abstraction. I don't recommend this.

    2. Move the database initialization to the repository. You can test if the database already has the needed data and insert it otherwise. You can use the DAO for this.

      This makes the initial database state dependent on the app logic. You dont have a hard guarantee that the database cannot be accessed before it is populated. There may be edge cases where this will fall on your feet. I don't recommend this.

    3. The official recommendation is to use a prepackaged database. That is part of the Room database builder (createFromAsset("database/myapp.db")) and efficient even for large and complex initial data. It will neatly integrate with destructive database migrations. You need to provide a complete prepopulated database file though.


    In addition, you might want/need to replace the LiveData with Flow. LiveData was the way to go using Java. In Kotlin you should use Flows instead. They are way more powerful and are recommended when using Compose.

    Just replace LiveData with Flow in your DAO and Repository. In the view model the flow needs to be converted into a StateFlow:

    val categories: StateFlow<List<Category>> = categoryRepository.getAll()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5.seconds),
            initialValue = emptyList(),
        )
    

    And in your composable, the flow gets collected like this:

    val categories by categoryViewModel.categories.collectAsStateWithLifecycle()
    

    Make sure you have the gradle dependency androidx.lifecycle:lifecycle-runtime-compose in your build files.


    1 If you still want to use this, make sure to get rid of Executors.newSingleThreadExecutor().execute. That is the Java way to run code in a separate thread, but you are in Kotlin here, and Kotlin uses coroutines. Obtain a coroutine scope (maybe let Hilt inject it) and launch a new coroutine in that scope.