5

I'm using the Room and Paging libraries to display categories.

My Entity:

@Entity(tableName = Database.Table.CATEGORIES)
data class Category(
    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) var id: Long = 0,
    @ColumnInfo(name = NAME) var name: String = "",
    @ColumnInfo(name = ICON_ID) var iconId: Int = 0,
    @ColumnInfo(name = COLOR) @ColorInt var color: Int = DEFAULT_COLOR
)

My DAO:

@Query("SELECT * FROM $CATEGORIES")
fun getPagedCategories(): DataSource.Factory<Int, Category>

@Update
fun update(category: Category)

My Repo:

val pagedCategoriesList: LiveData<PagedList<Category>> = categoryDao.getPagedCategories().toLiveData(Config(CATEGORIES_LIST_PAGE_SIZE))

My ViewModel:

val pagedCategoriesList: LiveData<PagedList<Category>>
    get() = repository.pagedCategoriesList

My Adapter:

class CategoriesAdapter(val context: Context) : PagedListAdapter<Category, CategoriesAdapter.CategoryViewHolder>(CategoriesDiffCallback()) {

    //region Adapter

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder {
        return CategoryViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_category, parent, false))
    }

    override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) {
        holder.bind(getItem(position)!!)
    }

    //endregion

    //region Methods

    fun getItemAt(position: Int): Category = getItem(position)!!

    //endregion

    inner class CategoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        private val iconHelper = IconHelper.getInstance(context)

        fun bind(category: Category) {
            with(itemView) {
                txvCategoryItemText.text = category.name
                imvCategoryItemIcon.setBackgroundColor(category.color)
                iconHelper.addLoadCallback {
                    imvCategoryItemIcon.setImageDrawable(iconHelper.getIcon(category.iconId).getDrawable(context))
                }
            }
        }
    }

    class CategoriesDiffCallback : DiffUtil.ItemCallback<Category>() {

        override fun areItemsTheSame(oldItem: Category, newItem: Category): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Category, newItem: Category): Boolean {
            return oldItem == newItem
        }
    }
}

And my Fragment:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    categoryViewModel = ViewModelProviders.of(this).get(CategoryViewModel::class.java)

    adapter = CategoriesAdapter(requireContext())
    categoryViewModel.pagedCategoriesList.observe(this, Observer(adapter::submitList))
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ViewCompat.setTooltipText(fabNewCategory, getString(R.string.NewCategory))

    with(mRecyclerView) {
        layoutManager = GridLayoutManager(requireContext(), 4)
        itemAnimator = DefaultItemAnimator()
        addItemDecoration(SpacesItemDecoration(resources.getDimensionPixelSize(R.dimen.card_default_spacing)))

        addOnItemTouchListener(OnItemTouchListener(requireContext(), this, this@CategoriesFragment))
    }

    mRecyclerView.adapter = adapter

    fabNewCategory.setOnClickListener(this)
}

Everything works when inserting, deleting or just loading categories. But when I'm updating a single entity's color or text, the list is not updated, though submit list is called correctly.

I debugged the whole process and found the problem: After submitting the list, AsyncPagedListDiffer#submitList is called. I compared the previous list (mPagedList in AsyncPagedListDiffer) and the new list (pagedListin AsyncPagedListDiffer#submitList). The items I edited there are equal and do already hold the new data. So DiffUtil compares everything and the items are already equal though the displayed list is not updated.

If the list is a reference, it would explain why the data is already refreshed in the adapters list, but how do I solve the issue then?

the_dani
  • 2,466
  • 2
  • 20
  • 46

3 Answers3

12

I think the problem is not the way you are loading the new data, but updating the data. Although you haven't show us the part where you triggers item update or how the actual update happens, I am guessing, sorry if I was wrong, you might be directly editting the list element like this:

category = adapter.getItemAt(/*item position*/)
category.name = "a new name"
category.color = 5
categoryViewModel.update(category)


Instead, you should create a new Category object instead of modifying existing ones, like this:

prevCategory = adapter.getItemAt(/*put position*/) // Do not edit prevCategory!
newCategory = Category(id=prevCategory.id, name="a new name", color=5, iconId=0)
categoryViewModel.update(newCategory)


The idea of creating a whole new fresh object every time you want to make even the smallest change is something might not be so obvious at first, but this reactive implementation relies on the assumption that each event is independent of other events. Making your data class immutable, or effectively immutable will prevent this issue.

What I like to do to avoid this kind of mistake, I always make every field in my data class final.

@Entity(tableName = Database.Table.CATEGORIES)
data class Category(
    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = ID) val id: Long = 0,
    @ColumnInfo(name = NAME) val name: String = "",
    @ColumnInfo(name = ICON_ID) val iconId: Int = 0,
    @ColumnInfo(name = COLOR) @ColorInt val color: Int = DEFAULT_COLOR
)
Sanlok Lee
  • 3,404
  • 2
  • 15
  • 25
  • 3
    Well, that was it.. Thank you very much! – the_dani Feb 03 '19 at 19:07
  • i had a similar issue i tried to fix for several days. i knew the problem had to do with something about references vs the objects themselves but no matter what i tried it didnt work (including making copies of the objects)... somehow, and i dont understand how, THIS WORKED! – or_dvir May 19 '19 at 15:58
  • I made the same mistake of editing original models since it's a list they are accessed by reference, hence the diff util was looking at the same items essentially. I just used model.copy(changedProperty = newPropert) and that solved the issue. – abdu Jan 21 '20 at 09:10
  • 1
    the solution is not appropriate. We want to use ListAdapter with LiveData. What is the point of updating the list item a second time when liveData is coming to notify the adapter with the new updated list. It's just a hacked solution. – Userlambda Feb 24 '20 at 09:33
  • @Mohamed, I am open to suggestions for edits but this is the idiomatic approach to any reactive framework. Nothing has been hacked. Also, the answer does not update the list more than once. – Sanlok Lee Feb 24 '20 at 10:27
  • Unfortunatly I do not have response. But I can't see myself replacing item on a list with excactly the same values. Because LiveData do all this work for us. The problem is coming before `submitList()` is called. Because when I place a breakpoint on this function, at the launch of this function oldItem has already replacing the oldList by the new one. – Userlambda Feb 24 '20 at 10:58
  • @Mohamed, it's important to understand what `LiveData` doesn't do. `LiveData` does not automatically deepcopy messages, and therefore it will fail when the source stream simply emits edited message and the observer tries to compare current message with the previous message, as in `DiffUtil`. I think creating a separate StackOverFlow question will help. – Sanlok Lee Feb 24 '20 at 11:29
  • 1
    Sorry but I still don't understand why before "submitList" is called ListAdapter already has the whole new list. – Userlambda Feb 24 '20 at 11:56
  • 2
    2:30am here, been trying to work around this issue for at least 6h and this was exactly it and in hindsight it makes sense since the current object is held in the list that is being used currently, meaning any comparison between the old and new list will already contain the changes in the old list that exist in the new list. – Shadow Jul 30 '20 at 06:40
  • I still don't understand it if the `ListAdapter` has the `updated` list before calling `submitList()` Why isn't the recyclerView updated ? – Saiprasad Prabhu Aug 14 '20 at 19:25
2

Nobody is able to answer your question unless you show your Dao and pagedlistadapter class which contains DiffUtill.itemcallaback. I show you some code might that help.

  1. you have to implement update in your DAO interface like this:

    @Update fun updateUsers(data: MyData)

if you have this method after that you check your diffcall back like below:

companion object {
    val videosDiffCallback = object : DiffUtil.ItemCallback<Item>(){
        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem.id == newItem.id //Called to decide whether two objects(new and old items) represent the same item.
        }

        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem == newItem //Called to decide whether two items have the same data.
        }
    }
}
//oldItem   Value: The item in the old list.
//newItem   Value: The item in the new list.
Hussnain Haidar
  • 2,200
  • 19
  • 30
  • I'm sorry, I thought the update dao is not important since it's standard-room-code. Please have a look at my edits! – the_dani Feb 03 '19 at 12:16
0

I have made a RnD on PagedListAdapter and custom paging with Room database. Click here and you will found my implementation. Hope this will help you.

Thanks.

Anjan Debnath
  • 120
  • 1
  • 8