Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 474
the_dani Avatar asked Feb 02 '19 14:02

the_dani


2 Answers

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
)
like image 194
Sanlok Lee Avatar answered Oct 13 '22 10:10

Sanlok Lee


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.
like image 33
Hussnain Haidar Avatar answered Oct 13 '22 11:10

Hussnain Haidar