【问题标题】:ViewPager (RecyclerView) with custom item count doesn't update data properly具有自定义项目计数的 ViewPager (RecyclerView) 无法正确更新数据
【发布时间】:2023-01-10 23:22:42
【问题描述】:

我为我的 RecyclerView 创建了一个适配器(使用 DiffUtil.ItemCallback 扩展 ListAdapter)。它是一个具有多个 itemViewType 的普通适配器,但它应该像循环一样,如果 API 发送标志并且数据集大小 > 1(通过覆盖 getItemCount() 以在条件 == true 时返回 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)
        }
    }
}
}

适配器实用程序代码:

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 以使其在更新前稍等片刻(没有帮助)

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

【问题讨论】:

    标签: android android-recyclerview android-adapter android-viewpager2


    【解决方案1】:

    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

    因此,当您 look at the actual source codeAsyncListDiffer 时,您会注意到它确实在其代码块的开头:

            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 与您的适配器具有相同的引用,因此 differ 过早退出,并且不会向适配器发送任何更改。

    很晦涩,我知道。

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

    大多数用户都这样做

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-10-23
      • 1970-01-01
      • 1970-01-01
      • 2015-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多