androidkotlinandroid-jetpack-composeandroid-roomkotlin-coroutines

How to propagate the changes from Room when I perform an update?


I have 7 items in Room and I want to update the name of one item. Here is what I have tried:

interface ItemDao {
    @Query("SELECT * FROM item_table")
    fun getItems(): Flow<List<Item>>

    @Update
    fun updateItem(item: Item)
}

My repo is very simple:

interface ItemRepository {
    fun getItems(): Flow<List<Item>>

    fun updateItem(item: Item)
}

Inside the ViewModel I have:

class ItemsViewModel @Inject constructor(
    private val repo: ItemRepository
) : ViewModel() {
    val itemsResultFlow = flow {
        repo.getItems().collect { items ->
            try {
                emit(Result.Success(items))
            } catch (e: Exception) {
                emit(Result.Failure(e))
            }
        }
    }

    var updateItemResult by mutableStateOf<Result<Unit>>(Result.Loading)
        private set

    fun updateItem(item: Item) = viewModelScope.launch {
        updateItemResult = try {
            Result.Success(repo.updateItem(item))
        } catch (e: Exception) {
            Result.Failure(e)
        }
    }
}

And inside the composable I have:

val itemsResponse by viewModel.itemsResultFlow.collectAsStateWithLifecycle(Result.Loading)

Scaffold(
    topBar = {
        //  
    },
    content = {
        when(val itemsResponse = itemsResponse) {
            is Result.Loading -> CircularProgressIndicator()
            is Result.Success -> {
                items(
                    items = itemsResponse.data
                ) { item ->
                    Text(item.name)
                }
            }
            is Result.Error -> print(result.e)
        }
    }
)

The app works fine but it loads the entire list of items each time I update a single item, which is not what I want. To get rid of this, I made some changes in ViewModel:

private var _items = MutableStateFlow<Result<List<Item>>>(Result.Loading)
val items: StateFlow<Result<List<Item>>> = _items.asStateFlow()

init {
    viewModelScope.launch {
        repo.getItems().collect { items ->
            try {
                _items.value = Response.Success(items)
            } catch (e: Exception) {
                _items.value = Response.Failure(e)
            }
        }
    }
}

And inside the composable:

val itemsResponse by viewModel.items.collectAsState()

The app compiles without any errors, but when I update the name of an item, I cannot see the updated name in the list, unless I close and reopen the app. How to see the updated value in the list without the need to load the entire list of items?

Update

Here is the call in the repo class:

fun getItems(): Flow<List<Item>>

And here is the call in the repo implementation class:

override fun getItems() = itemDao.getItems()

Solution

  • [...] it loads the entire list of items each time I update a single item, which is not what I want.

    I'm note sure about the reasons why you do not want this, but in general you should let the database optimize how to update the flows if something changed. Only when you have manifest performance issues you should refactor your app to address this.

    One way would be to remove the flows from your DAO so the list isn't automatically updated anymore. You would then need to do the updates yourself whenever a change occurs. Make sure to encapsulate this logic in the repository so the rest of your app still only has a Single Source of Truth (i.e. the Repo should still emit a Flow<List<Item>>, it just needs to construct the list itself whenever an item is changed).

    If your issue is caused by a very large list of thousands and thousands of items (it probably is not because you say you only have 7, but nevertheless..) and you cannot restrict them using a WHERE clause in your DAO, then the proper way would be to use the Paging library. In that case the database will only return chunked data, effectively providing multiple sublists. When an item is changed only the sublist that contains the item will be retrieved again.

    The solution you proposed doesn't address the issue because it still collects the getItems() flow. So whatever the rest of the code does, you already get an entire new list when a single item changes (which, just to emphasize it again, usually isn't a problem).

    And as a final note, please do not use MutableState in the view model, there are some nasty edge cases where this won't work as intended. You can replace it with MutableStateFlow if you want to keep the same functionality.