android 具有自定义项目计数的ViewPager(RecyclerView)未正确更新数据

rvpgvaaj  于 2023-01-11  发布在  Android
关注(0)|答案(1)|浏览(124)

我创建了一个适配器(使用DiffUtil.ItemCallback扩展ListAdapter)。它是一个具有多个itemViewType的普通适配器,但如果API发送标志并且数据集大小大于1,则它应该是smth循环适配器(通过重写getItemCount()在conditions == true时返回1000)。当我通过应用设置更改应用区域设置时,我的片段会重新创建,数据会异步加载(被动地,一行中多次,来自不同的请求,取决于几个rx字段,这导致数据集在区域设置更改后立即成为不同语言的数据组合(最终所有数据集都被正确翻译)(由于特性的特殊性,使其更像是同步的是不可能的)),将其值发布到LiveData,这将触发回收器视图的更新,问题出现了:

在最后一个数据集更新后,一些视图(最接近当前显示的视图和当前显示的视图)似乎未被翻译。

发布到LiveData的最终数据集被正确翻译,它的id中甚至有正确的locale标记。同样,在视图被回收并返回到它们之后-它们也是正确的。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)
        }
    }
}
}

查看寻呼机代码示例:

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

适配器实用程序代码:

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

已尝试不使用RecycleView的默认itemView变量(变得更糟)。尝试让diff utils总是返回false,以检查它是否正确计算了diff(是,正确)尝试将区域设置标记添加到数据集项目的ID(未帮助解决)在设置新数据之前尝试在区域设置更改时发布空数据集(我真惭愧,根本不应该考虑这个问题)尝试在rx中添加去抖动,让它在更新前等待一段时间(没有帮助)

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

whitzsjs

whitzsjs1#

ListAdapter的问题在于,它没有清楚地说明您需要提供一个新列表才能使其正常工作。
换句话说,the documentation says:(我引用源代码):

/**
     * 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执行look at the actual source code时,您会注意到它在其代码块的开头执行了以下操作:

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与适配器具有相同的引用,因此diffect过早退出,并且不向适配器分派任何更改。
我知道,很不起眼。
正如许多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)

相关问题