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")
}
)
}
}
}
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:
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.
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.
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.