PagedListAdapter не обновляет список, если изменяется только содержимое элемента

Я использую библиотеки Room и Paging для отображения категорий.

Моя сущность:

@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
)

Мой DAO:

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

@Update
fun update(category: Category)

Мое репо:

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

Моя модель просмотра:

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

Мой адаптер:

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
        }
    }
}

И мой фрагмент:

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)
}

Все работает при вставке, удалении или просто загрузке категорий. Но когда я обновляю цвет или текст отдельной сущности, список не обновляется, хотя список отправки вызывается правильно.

Я отладил весь процесс и обнаружил проблему: после отправки списка вызывается AsyncPagedListDiffer#submitList. Я сравнил предыдущий список (mPagedList в AsyncPagedListDiffer) и новый список (pagedListin AsyncPagedListDiffer#submitList). Элементы, которые я редактировал, идентичны и уже содержат новые данные. Таким образом, DiffUtil сравнивает все, и элементы уже равны, хотя отображаемый список не обновляется.

Если список является справочным, он объяснит, почему данные уже обновлены в списке адаптеров, но как мне тогда решить проблему?


person the_dani    schedule 02.02.2019    source источник
comment
Не могли бы вы поделиться своей CategoriesAdapter реализацией? Как вы реализовали DiffUtil.ItemCallback?   -  person Sanlok Lee    schedule 03.02.2019
comment
@SanlokLee Посмотреть мои правки   -  person the_dani    schedule 03.02.2019


Ответы (3)


Я думаю, проблема не в том, как вы загружаете новые данные, а в их обновлении. Хотя вы не показали нам, в какой части запускается обновление элемента или как происходит фактическое обновление, я предполагаю, извините, если я ошибался, вы можете напрямую редактировать элемент списка следующим образом:

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


Вместо этого вы должны создать новый Category объект вместо изменения существующих, например:

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)


Идея создания совершенно нового нового объекта каждый раз, когда вы хотите внести хотя бы малейшее изменение, поначалу может быть не так очевидна, но эта реактивная реализация основана на предположении, что каждое событие не зависит от других событий. Создание неизменяемого или фактически неизменяемого класса данных предотвратит эту проблему.

Что мне нравится делать, чтобы избежать такой ошибки, я всегда делаю каждое поле в моем классе данных окончательным.

@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
)
person Sanlok Lee    schedule 03.02.2019
comment
Ну вот и все .. Большое спасибо! - person the_dani; 03.02.2019
comment
У меня была аналогичная проблема, которую я пытался исправить в течение нескольких дней. Я знал, что проблема связана с ссылками на сами объекты, но независимо от того, что я пробовал, это не сработало (включая создание копий объектов) ... каким-то образом, и я не понимаю, как ЭТО РАБОТАЕТ! - person or_dvir; 19.05.2019
comment
Я совершил ту же ошибку, редактируя исходные модели, поскольку это список, к которому они обращаются по ссылке, поэтому утилита diff рассматривала в основном те же элементы. Я просто использовал model.copy (changedProperty = newPropert), и это решило проблему. - person abdu; 21.01.2020
comment
решение не подходит. Мы хотим использовать ListAdapter с LiveData. Какой смысл обновлять элемент списка во второй раз, когда liveData приходит, чтобы уведомить адаптер с новым обновленным списком. Это просто взломанное решение. - person Mohamed; 24.02.2020
comment
@Mohamed, я открыт для предложений по редактированию, но это идиоматический подход к любой реактивной структуре. Ничего не взламывали. Также ответ не обновляет список более одного раза. - person Sanlok Lee; 24.02.2020
comment
К сожалению, у меня нет ответа. Но я не могу представить, как заменяю элемент в списке точно такими же значениями. Потому что LiveData делает всю эту работу за нас. Проблема возникает до вызова submitList(). Потому что, когда я устанавливаю точку останова на этой функции, при запуске этой функции oldItem уже заменяет oldList новым. - person Mohamed; 24.02.2020
comment
@Mohamed, важно понимать, чего LiveData не делает. LiveData не выполняет автоматическое глубокое копирование сообщений, и поэтому он завершится ошибкой, когда исходный поток просто испускает отредактированное сообщение, а наблюдатель пытается сравнить текущее сообщение с предыдущим сообщением, как в DiffUtil. Думаю, поможет создание отдельного вопроса о StackOverFlow. - person Sanlok Lee; 24.02.2020
comment
Извините, но я все еще не понимаю, почему до вызова submitList ListAdapter уже имеет весь новый список. - person Mohamed; 24.02.2020
comment
2:30 утра здесь, я пытался обойти эту проблему в течение как минимум 6 часов, и это было именно то, и в ретроспективе это имеет смысл, поскольку текущий объект содержится в списке, который используется в настоящее время, что означает любое сравнение между старым и новым list уже будет содержать изменения в старом списке, которые существуют в новом списке. - person Shadow; 30.07.2020
comment
Я все еще не понимаю, есть ли ListAdapter список updated перед вызовом submitList() Почему не обновляется recyclerView? - person Saiprasad Prabhu; 14.08.2020

Никто не сможет ответить на ваш вопрос, если вы не покажете свой класс Dao и pagedlistadapter, который содержит DiffUtill.itemcallaback. Я покажу вам код, который может помочь.

  1. вы должны реализовать обновление в своем интерфейсе DAO следующим образом:

    @Update fun updateUsers (данные: MyData)

если у вас есть этот метод, после этого вы проверите свой diffcall, как показано ниже:

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.
person Hussnain Haidar    schedule 03.02.2019
comment
Извините, я думал, что обновление dao не важно, так как это стандартный код комнаты. Пожалуйста, посмотрите мои правки! - person the_dani; 03.02.2019

Я сделал RnD на PagedListAdapter и пользовательское разбиение на страницы с базой данных Room. Щелкните здесь, и вы найдете мою реализацию. Надеюсь, что это поможет вам.

Спасибо.

person Anjan Debnath    schedule 05.02.2019