Android Room database not updating on button click

148 views Asked by At

I am trying to update values in a room database when a button is clicked in the UI. I am creating an inventory app that allows the user to increase the number of item pieces by one each time a button is clicked. The new number of pieces should be displayed in the UI and the database should be updated.

I have created a method to update the database in the Dao. I have confirmed that the id/primaryKey of the target row and the model are similar. I have also tried inserting (with onConflictStategy.REPLACE) instead of updating. The program works fine and there are no compile-time errors however clicking the button does not update the database. Help me figure out what I am missing. Here is the code.

Entity

/**
 * Entity data class represents a single row in the database.
 */

@Entity(tableName = "invent")
data class Item (
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val name: String,
    val category: String,
    val unit: String,
    val pieces: Int,
    @ColumnInfo(name = "piecesBought", defaultValue = "0")
    val piecesBought: Int,
    val price: Int
    )

Dao

@Dao
interface ItemDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(item: Item)

    @Update(onConflict = OnConflictStrategy.REPLACE)
    suspend fun update(item: Item)

    @Delete
    suspend fun delete(item: Item)

    @Query("SELECT * from invent ORDER BY name ASC")
    fun getAllItems(): Flow<List<Item>>

    @Query("SELECT * from invent WHERE id = :id")
    fun getItem(id: Int): Flow<Item?>

    @Query("SELECT * from invent WHERE name like '%'||:name||'%'")
    fun getItemByName(name: String): Flow<List<Item?>>
}

Repository

class OfflineItemRepository(private val itemDao: ItemDao): ItemRepository {

    override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()
    override fun getItemByNameStream(name: String): Flow<List<Item?>> = itemDao.getItemByName(name)

    override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)

    override suspend fun insertItem(item: Item) = itemDao.insert(item)

    override suspend fun deleteItem(item: Item) = itemDao.delete(item)

    override suspend fun updateItem(item: Item) = itemDao.update(item)
}

viewModel

The function increasePiecesBoughtByOne is called when the button in the UI is clicked.

class ShoppingViewModel (itemRepository: ItemRepository): ViewModel() {

    private val myRepo = itemRepository

    private var _piecesBought = MutableStateFlow("")
    val piecesBought = _piecesBought.asStateFlow()

    /**
     * function to handle adding items to basket on + button click
     * enables the Added to basket button
     */
    fun increasePiecesBoughtByOne(item: Item) {
        // run database operations inside a coroutine.
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
            when  {
                item.piecesBought < item.pieces -> {
                    myRepo.updateItem(item.copy(piecesBought = (item.piecesBought + 1)))
                    _piecesBought.value = item.piecesBought.toString()
             
                }
            }
            }
        }
    }
}


UI

The value of TextField is given by the piecesBought variable from the viewModel as shown below. I want the piecesBought value to be increased by one each time onAddItem(item) is called when the button is clicked but it is not working.

The function increasePiecesBoughtByOne is called when the button in the TextField is clicked. I have confirmed that the function is called when the button is clicked by checking Logcat. However piecesBought does not change. Why isn't it changing?

val piecesBought by viewModel.piecesBought.collectAsStateWithLifecycle()

TextField(
                value = piecesBought,
                onValueChange = {
                    onPiecesBoughtChange(it, item)
                                },
                singleLine = true,
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
                modifier = modifier.weight(1f, fill = true)
            )
            Spacer(modifier.size(ButtonDefaults.IconSpacing))
            Icon(imageVector = Icons.Default.AddCircle,
                contentDescription = "Add item to cart",
                modifier = modifier.clickable {
                    onAddItem(item)
                }
            )

Database

Database migration from version 1 to version 2 involved creating a new column piecesBought

@Database(
    entities = [Item::class],
    version = 2,
    exportSchema = true,
    autoMigrations = [AutoMigration(from = 1, to = 2)]
)

abstract class DukaDatabase: RoomDatabase() {

    abstract fun itemDao(): ItemDao

    companion object {
        @Volatile
        private var Instance: DukaDatabase? = null

        fun getDatabase(context: Context): DukaDatabase {
            // if the Instance is not null, return it, otherwise create a new database instance.
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, DukaDatabase::class.java, "duka_database")
                    .createFromAsset("database/inventory_db.db")
                    //.fallbackToDestructiveMigration()
                    .build().also { Instance = it }
            }
        }
    }
}
1

There are 1 answers

0
Alele On

The UI was not updating because I was not using flow to update it as pointed out by Arek KubiƄski in the comments. I resolved this by combining 2 flows which would cause a UI update if any of their values changed. The 2 flows are newPieces and _searchText in the code below. I will focus on only one of the flows which is triggered when the button is clicked (newPieces). The other flow (_searchText) is tied to a TextField and is updated when the text in the TextField changes. It operates like a searchfield and helps filter searchList which is displayed in the UI.

Combined flow

Below is the combined flow which updates searchList which is the list displayed by the UI. If the _searchText variable or _newPieces variable changed, it would trigger a call to update searchList.

val searchList = _searchText.combine(newPieces) {text, _ ->
        withContext(Dispatchers.IO){ itemRepo.getItemByNameStream(text).first() }
    }.stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
                initialValue = null
            )

Function to increase count on button click

This is the function that is called when the button is clicked. As part of this function body I included _newPieces.emit(item.piecesBought) that emits a value each time the function is called. This results in a change of the newPieces sharedFlow value. This is what causes an update of searchList and a change in the UI.

/**
     * function to handle adding items to basket on + button click
     * enables the Added to basket button
     */
    fun increasePiecesBoughtByOne(item: Item) {
        // run database operations inside a coroutine.
        val newPiecesBought: Int = item.piecesBought.inc()
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
            when  {
                item.piecesBought < item.pieces -> {
                    itemRepo.updateItem(item.copy(piecesBought = newPiecesBought))
                    // emitted value does not matter
                    _newPieces.emit(item.piecesBought)
                }
            }
            }
        }
    }

MutableSharedFlow

newPieces.collect{} collects the new value emitted by _newPieces.emit(item.piecesBought) and triggers an update of the UI through a change in searchList

/**
     * Using MutableSharedFlow because MutableStateFlow does not emit the same value consecutively
     * MutableSharedFlow does not take an initial value
     */
    private val _newPieces = MutableSharedFlow<Int>(
        replay = 1,
        extraBufferCapacity = 1,
        onBufferOverflow = BufferOverflow.DROP_OLDEST
    )

    private val newPieces = _newPieces.asSharedFlow()
    init {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                newPieces.collect{
                }
            }
        }

        // providing initial value to be available to combine transformation
        // combine needs values from all the flows (_searchText and newPieces) in order to trigger transformation
        viewModelScope.launch {
            withContext(Dispatchers.IO) {_newPieces.emit(0)}
        }
    }