具有自定义项目计数的视图寻呼机 (回收器视图) 无法正确更新数据



我已经为我的RecyclerView创建了一个适配器(使用DiffUtil.ItemCallback扩展ListAdapter)。它是一个具有多个itemViewTypes的普通适配器,但如果API发送标志并且数据集大小>1(当条件==true时,通过重写getItemCount()返回1000)。当我通过应用程序设置更改应用程序区域设置时,我的片段会重新创建,数据异步加载(根据几个rx字段,从不同的请求连续多次响应加载,这导致数据集在区域设置更改后成为不同语言的数据的组合(最终所有数据集都被正确地翻译成btw)(由于功能的特殊性,使其更像是不可能同步)),将其值发布到LiveData,它触发了回收器视图的更新,问题出现了:

上次数据集更新后,某些视图(最接近当前显示和当前显示的视图)似乎无法翻译

发布到LiveData的最终数据集被正确翻译,它的id中甚至有正确的区域设置标记。此外,在视图被回收并返回到它们之后,它们也是正确的。DiffUtil的计算也正确(我试图在项回调中只返回false,而回收器视图仍然没有正确更新其视图持有者)。当itemCount==list.size时,一切正常。当适配器假装是循环的并且itemCount==1000时-否。有人能解释这种行为并帮助解决这个问题吗?

适配器代码示例:

private const val TYPE_0 = 0
private const val TYPE_1 = 1
class CyclicAdapter(
val onClickedCallback: (id: String) -> Unit,
val onCloseClickedCallback: (id: String) -> Unit,
) : ListAdapter<IViewData, RecyclerView.ViewHolder>(DataDiffCallback()) {
var isCyclic: Boolean = false
set(value) {
if (field != value) {
field = value
}
}
override fun getItemCount(): Int {
return if (isCyclic) {
AdapterUtils.MAX_ITEMS // 1000
} else {
currentList.size
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_0 -> Type0.from(parent)
TYPE_1 -> Type1.from(parent)
else -> throw ClassCastException("View Holder for ${viewType} is not specified")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is Type0 -> {
val item = getItem(
AdapterUtils.actualPosition(
position,
currentList.size
)
) as ViewData.Type0
holder.setData(item, onClickedCallback)
}
is Type1 -> {
val item = getItem(
AdapterUtils.actualPosition(
position,
currentList.size
)
) as ViewData.Type1
holder.setData(item, onClickedCallback, onCloseClickedCallback)
}
}
}
override fun getItemViewType(position: Int): Int {
return when (val item = getItem(AdapterUtils.actualPosition(position, currentList.size))) {
is ViewData.Type0 -> TYPE_0
is ViewData.Type1 -> TYPE_1
else -> throw ClassCastException("View Type for ${item.javaClass} is not specified")
}
}
class Type0 private constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun setData(
viewData: ViewData.Type0,
onClickedCallback: (id: String) -> Unit
) {
(itemView as Type0View).apply {
acceptData(viewData)
setOnClickedCallback { url ->
onClickedCallback(viewData.id,)
}
}
}
companion object {
fun from(parent: ViewGroup): Type0 {
val view = Type0View(parent.context).apply {
layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
return Type0(view)
}
}
}
class Type1 private constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) {
fun setData(
viewData: ViewData.Type1,
onClickedCallback: (id: String) -> Unit,
onCloseClickedCallback: (id: String) -> Unit
) {
(itemView as Type1View).apply {
acceptData(viewData)
setOnClickedCallback { url ->
onClickedCallback(viewData.id)
}
setOnCloseClickedCallback(onCloseClickedCallback)
}
}
companion object {
fun from(parent: ViewGroup): Type1 {
val view = Type1View(parent.context).apply {
layoutParams =
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}
return Type1(view)
}
}
}
}

ViewPager代码示例:

class CyclicViewPager @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
ICyclicViewPager {
private val cyclicViewPager: ViewPager2
private lateinit var onClickedCallback: (id: String) -> Unit
private lateinit var onCloseClickedCallback: (id: String) -> Unit
private lateinit var adapter: CyclicAdapter
init {
LayoutInflater
.from(context)
.inflate(R.layout.v_cyclic_view_pager, this, true)
cyclicViewPager = findViewById(R.id.cyclic_view_pager)
(cyclicViewPager.getChildAt(0) as RecyclerView).apply {
addItemDecoration(SpacingDecorator().apply {
dpBetweenItems = 12
})
clipToPadding = false
clipChildren = false
overScrollMode = RecyclerView.OVER_SCROLL_NEVER
}
cyclicViewPager.offscreenPageLimit = 3
}
override fun initialize(
onClickedCallback: (id: String) -> Unit,
onCloseClickedCallback: (id: String) -> Unit
) {
this.onClickedCallback = onClickedCallback
this.onCloseClickedCallback = onCloseClickedCallback
adapter = CyclicAdapter(
onClickedCallback,
onCloseClickedCallback,
).apply {
stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
}
cyclicViewPager.adapter = adapter
}
override fun setState(viewPagerState: CyclicViewPagerState) {
when (viewPagerState.cyclicityState) {
is CyclicViewPagerState.CyclicityState.Enabled -> {
adapter.submitList(viewPagerState.pages) {
adapter.isCyclic = true
cyclicViewPager.post {
cyclicViewPager.setCurrentItem(
// Setting view pager item to +- 500
AdapterUtils.getCyclicInitialPosition(
adapter.currentList.size
), false
)
}
}
}
is CyclicViewPagerState.CyclicityState.Disabled -> {
if (viewPagerState.pages.size == 1 && adapter.isCyclic) {
cyclicViewPager.setCurrentItem(0, false)
adapter.isCyclic = false
}
adapter.submitList(viewPagerState.pages)
}
}
}
}

适配器Utils代码:

object AdapterUtils {
const val MAX_ITEMS = 1000
fun actualPosition(position: Int, listSize: Int): Int {
return if (listSize == 0) {
0
} else {
(position + listSize) % listSize
}
}
fun getCyclicInitialPosition(listSize: Int): Int {
return if (listSize > 0) {
MAX_ITEMS / 2 - ((MAX_ITEMS / 2) % listSize)
} else {
0
}
}
}

尝试不使用RecyclerView的默认itemView变量(变得更糟)。尝试使diff-utils始终返回false,以检查它是否正确计算diff(是,正确)尝试将区域设置标记添加到数据集项的ID中(没有帮助解决)在设置新数据之前,尝试发布关于区域设置更改的空数据集(真遗憾,我甚至不应该考虑它)尝试在rx中添加去抖动,使其在更新前等待一段时间(没有帮助)

UPD:当我手动调用adapter.notifyDatasetChanged()时,这不是首选的方式,一切都很好,所以问题是为什么ListAdapter在我的情况下没有正确地调度通知回调?

ListAdapter的问题是它没有明确地说明您需要提供一个新的列表才能运行。

换句话说,文档上写着:(我引用源代码):

/**
* Submits a new list to be diffed, and displayed.
* <p>
* If a list is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
*
* @param list The new list to be displayed.
*/
public void submitList(@Nullable List<T> list) {
mDiffer.submitList(list);
}

关键词是列表。

然而,正如您在那里看到的,适配器所做的只是服从DiffUtil并在那里调用submitList

因此,当您查看AsyncListDiffer的实际源代码时,您会注意到它确实如此,在其代码块的开头:

if (newList == mList) {
// nothing to do (Note - still had to inc generation, since may have ongoing work)
if (commitCallback != null) {
commitCallback.run();
}
return;
}

换句话说,如果新列表(引用)与旧列表相同,无论其内容如何,都不要执行任何操作

这听起来可能很酷,但这意味着如果你有这个代码,适配器将不会真正更新:

(伪…)

var list1 = mutableListOf(...) 
adapter.submitList(list1)
list1.add(...)
adapter.submitList(list1)

原因是list1与您的适配器具有相同的引用,因此difference提前退出,并且不会向适配器发送任何更改。

我知道,很晦涩。

正如许多SO答案所指出的,解决方案是创建列表本身的副本。

大多数用户进行

var list1 = mutableListOf(...) 
adapter.submitList(list1)
var list2 = list1.toMutableList()
list2.add(...)
adapter.submitList(list2)

toMutableList()的调用创建了一个包含list1项目的新列表,因此if (newList == mList) {上面的比较现在应该是false,并且应该执行正常代码。

更新

请记住,许多开发人员都会犯以下错误。。。

var list = mutableListOf...
adapter.submitList(list)
list.add(xxx)
adapter.submitList(list.toList())

这不起作用,因为您创建的新列表引用的对象与适配器的对象相同。这意味着,尽管列表listlist.toList()是ArrayList的两个实例,但它们都指向相同的东西。但副作用是DiffUtil会比较这些项,并且它们是相同的,因此也不会向适配器发送diff。

正确的顺序是…

val list = mutableListOf(...)
adapter.submitList(list.toList())
// Make a copy first, so we can alter it as we please without the *current list held by the adapter* from being affected.
var modified = list.toMutableList()
modified.add(...)
adapter.submitList(modified)

在GitHub中查看了您的示例后,我能够重现这个问题。只有大约30-40分钟的时间,我可以说我不能100%确定哪个组件没有更新。

我注意到的事情。

  1. 更改区域设置时不会调用onBindViewHolder方法(除了第一次?)。

  2. 我不明白为什么在你提交了回调中的列表后需要post到适配器:

cyclicViewPager.setCurrentItem(
// Setting view pager item to +- 500
AdapterUtils.getCyclicInitialPosition(
adapter.currentList.size
), false
)

为什么?这意味着用户将失去当前位置。

为什么不保留现有的?

  1. 我注意到你做了cyclicViewPager.offscreenPageLimit = 3,这有效地禁用了RecyclerView"逻辑";并使用通常的ViewPager状态适配器逻辑";预取/保持";N(在您的情况下为3)页;前进";。起初,我认为这会引起问题,但删除它(将其设置为-1,这是默认值和"use RecyclerView"值)并没有做出太大的更改(尽管我确实注意到这里和那里的一些更改,因为它有时会更新当前的更改,但不会在2-3页内更新下一个)

文件上写着:

设置应保留在当前可见页面两侧的页数。超过此限制的页面将在需要时从适配器重新创建。将其设置为OFFSCREN_PAGE_LIMIT_DEFAULT以使用RecyclerView的缓存策略。

所以我会想象默认值会得到ListAdapter及其DiffUtil的帮助。事实似乎并非如此。

  1. 所做的尝试(以及其他一些事情)是查看问题是否在实际适配器中(或者至少在viewPager对其适配器的依赖性中)。我没有时间(工作!),但我注意到,如果你这样做:
override fun setState(viewPagerState: CyclicViewPagerState) {
when (viewPagerState.cyclicityState) {
is CyclicViewPagerState.CyclicityState.Enabled -> {

// call initialize again, to recreate the adapter
initialize(this.onClickedCallback, this.onCloseClickedCallback)
adapter.submitList(viewPagerState.pages) {
adapter.isCyclic = true
// Setting vp item to ... (code omitted for brevity)
}

这是有效的。理论上,当您重新创建整个适配器时,效率较低,但在您的示例中,您有效地创建了一整组新的数据,更改了每个ID,因此就性能而言,我认为这更高效,因为不需要重新计算更改并调度它们,因为在Diff-Util看来,所有的行都是不同的。通过重新创建适配器,嗯。。。副总统无论如何都得重新任命。

我注意到这在你的例子中效果很好。

我接着又加了两件事,因为;愚蠢的";适配器无法可靠地告诉您当前的位置。。。你可以天真地保存它:

CyclicViewPager中:

var currentPos: Int = 0
init {
...
this.cyclicViewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int)
currentPos = position
}
})
}

然后

is CyclicViewPagerState.CyclicityState.Enabled -> {
initialize(this.onClickedCallback, this.onCloseClickedCallback)
adapter.submitList(viewPagerState.pages) {
adapter.isCyclic = true
if (adapter.currentList.size <= currentPos) {
cyclicViewPager.setCurrentItem(currentPos, false)
} else {
cyclicViewPager.setCurrentItem(
// Setting view pager item to +- 500
AdapterUtils.getCyclicInitialPosition(
adapter.currentList.size
), false
)
}
}
}

这确实有效,但当然,您要重新创建整个VP适配器,因此可能不需要。

在这一点上,我要么需要花更多的时间来弄清楚VP、RV或其依赖关系的哪一部分不是";调度";正确的数据。我的猜测可能是一些愚蠢的ViewPager优化,再加上安卓系统非常不可靠的View系统,不会在队列中挑选消息;但我可能也大错特错;)

我希望那些更聪明和/或系统中有更多咖啡的人能找到一个更简单的解决方案。

(总的来说,我发现样本项目相对容易导航,但数据的设计有点复杂,但是……由于它是一个样本,很难判断你真正拥有什么"现实生活中"的数据结构)。

相关内容

  • 没有找到相关文章

最新更新