Search code examples
androidkotlinandroid-jetpack-composeandroid-roomandroid-livedata

Jetpack compose Room database UI update issues


Seen a few posts about similar problems but nothing that made my code work.

I have a database that stores about a 100 objects. These are displayed on the screen but once I update one of the objects from the database by pressing a button the database does change the value (in the app inspector) but it is not reflected in the UI. Also, this change is only able to be done once, after that it wont let me update the same object in the database again.

@Composable
fun MainScreen(navController: NavController, cocktailViewModel: CocktailViewModel) {
    val allCocktails = cocktailViewModel.cocktailsFromDb.observeAsState(emptyList())

    LaunchedEffect(Unit) {
        cocktailViewModel.getAllCocktails()
    }

    LazyColumn(
        modifier = Modifier
            .padding(bottom = 110.dp),
        contentPadding = PaddingValues(
            start = 16.dp,
            end = 16.dp
        ),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(allCocktails.value) { drink ->
            Row(
                modifier = Modifier
                    .fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(text = drink.strDrink)
                Icon(
                    imageVector = if (drink.isBookmarked) Icons.Filled.Bookmark else Icons.Filled.BookmarkBorder,
                    contentDescription = "Bookmark",
                    tint = Color.Black,
                    modifier = Modifier
                        .size(40.dp)
                        .clickable {
                            cocktailViewModel.toggleBookmark(drink) // Here is the update
                        }
                )
            }
            HorizontalDivider(
                modifier = Modifier.padding(vertical = 4.dp),
                thickness = 1.dp,
                color = Color.Gray
            )
        }
    }
}

That is the view and here comes part of the Viewmodel:

class CocktailViewModel(private val cocktailDao: CocktailDAO) : ViewModel() {
    private val _cocktailsFromDb = MutableLiveData<List<ModifiedDrink>>()
    val cocktailsFromDb: LiveData<List<ModifiedDrink>> = _cocktailsFromDb

    fun toggleBookmark(drink: ModifiedDrink) {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val updatedDrink = drink.copy(isBookmarked = !drink.isBookmarked)
                cocktailDao.updateDrink(updatedDrink)
                Log.d("CocktailViewModel", "Successfully updated drink: ${updatedDrink.strDrink}")

                // Just for logging purposes
                val fetchedDrink = cocktailDao.getDrinkById(updatedDrink.idDrink)
                Log.d("CocktailViewModel", "Fetched drink by ID after update: $fetchedDrink")
            } catch (e: Exception) {
                Log.e("CocktailViewModel", "Error during toggleBookmark: ${e.message}", e)
            }
        }
    }

    fun getAllCocktails() {
        viewModelScope.launch {
            try {
                val drinks = withContext(Dispatchers.IO) {
                    cocktailDao.getAllDrinks()
                }
                _cocktailsFromDb.postValue(drinks)
            } catch (e: Exception) {
                Log.e("CocktailViewModel", "Error fetching drinks from database: ${e.message}", e)
            }
        }
    }
}

The DAO and Entity:

@Dao
interface CocktailDAO {
    @Query("SELECT * FROM Cocktails")
    fun getAllDrinks(): Flow<List<ModifiedDrink>>

    @Query("SELECT * FROM Cocktails WHERE idDrink = :id")
    suspend fun getDrinkById(id: Int): ModifiedDrink

    @Update
    suspend fun updateDrink(drink: ModifiedDrink)
}
@Entity(tableName = "Cocktails")
data class ModifiedDrink(
    @PrimaryKey val idDrink: Int,
    val strDrink: String,
    val isBookmarked: Boolean,
)

In my mind changing a drink in cocktailsFromDb in the viewmodel should create a UI update since I observe it in the view. However, this does not seem to work.


Solution

  • "[...] changing a drink in cocktailsFromDb in the viewmodel should create a UI update".

    That's correct. The only issue is that you never do change anything in cocktailsFromDb. The only time when you actually change the LiveData with _cocktailsFromDb.postValue(drinks) is in getAllCocktails(). And that is only called once, namely in the LaunchedEffect of MainScreen (Note that you passed Unit as the key for LaunchedEffect which will effectively prevent this from being executed again).

    What you really want to do is drop the LiveData alltogether (LiveData is obsolete when you use Compose) and replace that with a Flow. Actually, you should even use a Flow as the return value in your Dao:

    fun getAllDrinks(): Flow<List<ModifiedDrink>>
    

    Now you do not even need a dedicated getAllCocktails() function anymore because whenever something changes in the database, the flow will just automatically provide a new list of cocktails.

    Instead, your view model should just needs this property:

    val cocktails = cocktailDao.getAllDrinks()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList(),
        )
    

    The stateIn converts the flow to a StateFlow that has similar semantics to a LiveData as it can be observed for changes. Different to your LiveData this flow will now automatically update when anything changes in the database.

    In your composables you can now convert your flow into a State object like this:

    val allCocktails = cocktailViewModel.cocktails.collectAsStateWithLifecycle()
    

    You need the gradle dependency androidx.lifecycle:lifecycle-runtime-compose for that.

    Since there is no getAllCocktails() anymore you can now also remove the LaunchedEffect.


    The issue with upating the database was probably caused by the UI still having the previous ModifiedDrink object. So you would repeatedly call toggleBookmark with the same object, resulting in the same database update - for which only the first one would actually change anything. This should also be fixed along the way when your UI gets properly updated now.