PagedListAdapter does not update list if just the content of an item changes
Asked Answered
S

3

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?

Scheme answered 2/2, 2019 at 14:0 Comment(2)
Could you share your CategoriesAdapter implementation? How did you implement DiffUtil.ItemCallback?Aerial
@SanlokLee See my editsScheme
A
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
)
Aerial answered 3/2, 2019 at 16:36 Comment(10)
Well, that was it.. Thank you very much!Scheme
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!Chapeau
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.Plea
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.Serviette
@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.Aerial
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.Serviette
@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.Aerial
Sorry but I still don't understand why before "submitList" is called ListAdapter already has the whole new list.Serviette
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.Sadyesaechao
I still don't understand it if the ListAdapter has the updated list before calling submitList() Why isn't the recyclerView updated ?Squamous
W
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.
Weakkneed answered 3/2, 2019 at 4:51 Comment(1)
I'm sorry, I thought the update dao is not important since it's standard-room-code. Please have a look at my edits!Scheme
T
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.

Thermotaxis answered 5/2, 2019 at 9:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.