I have an App using latest dependencies and Gradle 8.3 with Kotlin DSL. The ViewModel is working fine to retrieve all data from the Room local SQLite Database. But doesn't work for retrieving single Recipe by id. The App compiles without errors. Issue found using Logcat.
Here is the code of different Kotlin files associated with this project.
RecipeDatabase.kt
@Database(
entities = [RecipeModel::class],
version = Constants.version,
exportSchema = false
)
abstract class RecipeDatabase: RoomDatabase() {
abstract fun recipeDao(): RecipeDAO
companion object{
@Volatile
private var INSTANCE: RecipeDatabase? = null
fun getDatabase(
context: Context
): RecipeDatabase{
val tempInstance = INSTANCE
if(tempInstance != null){
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(context, RecipeDatabase::class.java, Constants.database)
.createFromAsset(Constants.databaseLocation)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
}
}
RecipeDAO.kt
@Dao
interface RecipeDAO {
@Query("SELECT * FROM recipes ORDER BY id ASC")
fun readAllData(): LiveData<List<RecipeModel>> // THIS WORKS
@Query("SELECT * FROM recipes WHERE id=:id")
fun loadSingle(id: String): LiveData<RecipeModel> // THIS DOESNT
}
RecipeRepository.kt
class RecipeRepository(private val recipeDAO: RecipeDAO) {
val readAllData: LiveData<List<RecipeModel>> = recipeDAO.readAllData() // THIS WORKS
fun loadSingle(recipeId: String): LiveData<RecipeModel> {
return recipeDAO.loadSingle(recipeId)
}
}
RecipeViewModel
class RecipeViewModel(application: Application): AndroidViewModel(application) {
val readAllData: LiveData<List<RecipeModel>>
private val repository: RecipeRepository
lateinit var eventSingle: LiveData<RecipeModel>
init {
val recipeDAO = RecipeDatabase.getDatabase(application).recipeDao()
repository = RecipeRepository(recipeDAO)
readAllData = repository.readAllData // THIS WORKS
}
fun getSingle(recipeId: String) { // THIS DOESNT
eventSingle = repository.loadSingle(recipeId)
}
}
Composable RecipeScreen.kt
@Composable
fun RecipeScreen(navController: NavHostController, recipeViewModel: RecipeViewModel, recipeID: Int) {
println("RECIPE ID" + recipeID.toString()) // THIS WORKS
val recipeAvailable = recipeViewModel.getSingle(recipeID.toString()) // THIS DOESNT
val available by recipeViewModel.eventSingle.observeAsState() // THIS DOESNT
println(available) // <-- null
}
NavHost composable
composable(route = Recipe.route) {
backStackEntry ->
val recipeID = backStackEntry.arguments?.getString("recipeID")
if (recipeID != "") {
RecipeScreen(navController, recipeViewModel, recipeID!!.toInt())
}
}
The problem was caused by this code (how can I replace the Image composable so it waits even if it has drawable set to null at first?) :
@Composable
fun Hero(recipePreview: RecipeModel) {
val context = LocalContext.current
val imageName = recipePreview.image.replace(".jpg", "")
val drawableId = remember(imageName) {
context.resources.getIdentifier(
imageName,
"drawable",
context.packageName
)
}
Box(Modifier.height(280.dp)) {
Image(
painter = painterResource(id = drawableId),
contentDescription = recipePreview.title,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
contentScale = ContentScale.FillBounds,
)
}
}
When I remove Image the rest of the Recipe appears. So only the Image is the problem, because at first I have null.
Please help. Thanks in advance.
This might not be the only issue, but combining var
with LiveData is going to cause problems. The UI layer begins observing the first LiveData. When you change the property to point at a different LiveData, the UI layer doesn't know and just keeps observing the first LiveData.
To solve this, you can create a backing LiveData of the requested IDs, and switchMap
to produce the final map of single database rows that you want.
Updated ViewModel code:
class RecipeViewModel(application: Application): AndroidViewModel(application) {
val readAllData: LiveData<List<RecipeModel>>
private val repository: RecipeRepository
private val eventSingleId = MutableLiveData<String>()
val eventSingle: LiveData<RecipeModel> =
eventSingleId.switchMap { repository.loadSingle(it) }
init {
val recipeDAO = RecipeDatabase.getDatabase(application).recipeDao()
repository = RecipeRepository(recipeDAO)
readAllData = repository.readAllData
}
fun getSingle(recipeId: String) {
eventSingleId.value = recipeId
}
}
By the way, your singleton access of the database is vulnerable to instantiating two databases. It needs to be a double-checked lock. Update your code to do a second check for it having been instantiated inside the synchronized block:
fun getDatabase(
context: Context
): RecipeDatabase{
var instance = INSTANCE
if(instance != null){
return tempInstance
}
synchronized(this) {
instance = INSTANCE
if(instance != null){
return instance
}
instance = Room.databaseBuilder(context, RecipeDatabase::class.java, Constants.database)
.createFromAsset(Constants.databaseLocation)
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
return instance
}
}
Or to simplify the code using Elvis operators and also
:
fun getDatabase(
context: Context
): RecipeDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(context, RecipeDatabase::class.java, Constants.database)
.createFromAsset(Constants.databaseLocation)
.fallbackToDestructiveMigration()
.build()
.also { INSTANCE = it }
}