ListAdapter:提交List后未重新绑定项目单击侦听器

问题描述 投票:0回答:1

我对

ListAdapter
有一个问题,尽管它可能按预期工作,但对我来说却成了一个错误。

假设我有这个数据类

data class AssetMinDataDomain(
    val id: String = "",
    val metricsMinDomain: MetricsMinDomain = MetricsMinDomain(),
    val name: String = "",
    val symbol: String? = ""
) {

    var isSelected: Boolean = name == DEFAULT_ASSET // Default selected item in Market Cap

    companion object {
        const val DEFAULT_ASSET = "Bitcoin"
    }

}

我想重点关注

price
selected
状态的变化,因此我们在
DiffUtil
上进行此设置。

class DiffUtilAssetMin : DiffUtil.ItemCallback<AssetMinDataDomain>() {

    // DiffUtil uses this test to help discover if an item was added, removed, or moved.
    // Use attribute(s) that represent object's identity.
    override fun areItemsTheSame(
        oldItem: AssetMinDataDomain,
        newItem: AssetMinDataDomain
    ): Boolean {
        return oldItem.id == newItem.id
    }

    // Check whether oldItem and newItem contain the same data; that is, whether they are equal.
    // If there are differences between oldItem and newItem, this code tells DiffUtil that the item has been updated.
    // Note: If you are using data class and trying to detect changes based on properties outside primary constructor,
    // you may need to do additional checking since the default generated `equals` only uses properties inside primary constructor.
    override fun areContentsTheSame(
        oldItem: AssetMinDataDomain,
        newItem: AssetMinDataDomain
    ): Boolean {
        return oldItem == newItem && oldItem.isSelected == newItem.isSelected
    }

    override fun getChangePayload(oldItem: AssetMinDataDomain, newItem: AssetMinDataDomain): Any? {

        if (oldItem.id == newItem.id) {
            return if (oldItem.metricsMinDomain.priceUsd == newItem.metricsMinDomain.priceUsd
                && oldItem.isSelected == newItem.isSelected
            ) {
                super.getChangePayload(oldItem, newItem)
            } else {
                // Add object's attribute(s) that has changed using this payload
                Bundle().apply {
                    newItem.metricsMinDoman.priceUsd?.let {
                        putDouble(ARG_MARKET_PRICE, it)
                    }
                    putBoolean(ARG_IS_SELECTED, newItem.isSelected)
                }
            }
        }

        return super.getChangePayload(oldItem, newItem)

    }

    companion object {
        const val ARG_MARKET_PRICE = "arg.market.price"
        const val ARG_IS_SELECTED = "arg.is.selected"
    }
}

我们正在执行完全绑定或部分绑定,具体取决于

ViewHolder
是否被回收或调用
adapter.submitList(items)

后数据是否更新
class AssetMinAdapter(
    private val iconLink: String,
    private val glide: RequestManager,
    private val maximumSelectedAsset: Int,
    private val itemListener: ItemListener
) : FilterableListAdapter<AssetMinDataDomain, AssetMinAdapter.ItemView>(DiffUtilAssetMin()) {

    inner class ItemView(itemView: AssetMinCardBinding) : RecyclerView.ViewHolder(itemView.root) {
        internal val cardView = itemView.cardRoot
        private val assetName = itemView.assetName
        private val assetSymbol = itemView.assetSymbol
        private val assetPrice = itemView.assetPrice
        private val assetIcon = itemView.assetIcon

        // Full update/binding
        fun bindFull(domain: AssetMinDataDomain) {

            with(itemView.context) {

                bindTextData(
                    domain.name,
                    domain.symbol,
                    domain.metricsMinDomain.marketDataMinDomain.priceUsd,
                    domain.isSelected
                )

                glide
                    .load(
                        getString(
                            R.string.icon_url,
                            iconLink,
                            domain.id
                        )
                    )
                    .circleCrop()
                    .into(assetIcon)

            }

        }

        // Partial update/binding
        fun bindPartial(domain: AssetMinDataDomain, bundle: Bundle) {
            bindTextData(
                domain.name,
                domain.symbol,
                bundle.getDouble(DiffUtilAssetMin.ARG_MARKET_PRICE),
                bundle.getBoolean(DiffUtilAssetMin.ARG_IS_SELECTED)
            )
        }

        private fun bindTextData(name: String, symbol: String?, price: Double?, isSelected: Boolean) {

            with(itemView.context) {
                assetName.text = name
                assetSymbol.text = symbol ?: getString(R.string.empty)
                assetPrice.text =
                    getString(R.string.us_dollars, NumbersUtil.formatFractional(price))
                cardView.isChecked = isSelected
            }

        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemView =
        ItemView(
            AssetMinCardBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )

    override fun onBindViewHolder(holder: ItemView, position: Int) {
        onBindViewHolder(holder, holder.bindingAdapterPosition, emptyList())
    }

    override fun onBindViewHolder(holder: ItemView, position: Int, payloads: List<Any>) {

        with(holder) {

            val domain = getItem(bindingAdapterPosition)

            // Upon scroll we need to rebind click listener regardless if full or partial update
            // this is to ensure that click listener is bound to correct item.
            cardView.setOnClickListener {

                if (domain.name.equals(AssetMinDataDomain.DEFAULT_ASSET, true))
                    return@setOnClickListener

                val selectedSize = currentList.filter { it.isSelected }.size

                // To avoid abuse usage, limit the selectable asset market cap
                // this also including the default asset which is BTC
                if (selectedSize >= maximumSelectedAsset && !domain.isSelected) {
                    itemListener.onLimitReached()
                    return@setOnClickListener
                }

                domain.isSelected = !cardView.isChecked
                cardView.isChecked = domain.isSelected
                itemListener.onAssetSelected()

            }

            if (payloads.isEmpty() || payloads.first() !is Bundle)
                bindFull(domain) // Full update/binding
            else {

                val bundle = payloads.first() as Bundle

                bindPartial(domain, bundle) // Partial update/binding

            }

        }

    }

    // Required when setHasStableIds is set to true
    override fun getItemId(position: Int): Long {
        return currentList[position].id.hashCode().toLong()
    }

    override fun onFilter(
        list: List<AssetMinDataDomain>,
        constraint: String
    ): List<AssetMinDataDomain> {

        return list.filter {
            it.name.lowercase().contains(constraint.lowercase()) ||
            it.symbol?.lowercase()?.contains(constraint.lowercase()) == true ||
            it.name.equals(AssetMinDataDomain.DEFAULT_ASSET, true)
        }

    }

    // Since adapter.currentList() does not immediately reflecting the actual update from filters
    // we can use this callback instead to listen and get the latest list
    override fun onCurrentListChanged(
        previousList: List<AssetMinDataDomain>,
        currentList: List<AssetMinDataDomain>
    ) {
        super.onCurrentListChanged(previousList, currentList)
        itemListener.onListUpdate(previousList, currentList)
    }

    interface ItemListener {

        fun onAssetSelected()
        fun onLimitReached()
        fun onListUpdate(
            previousList: List<AssetMinDataDomain>,
            currentList: List<AssetMinDataDomain>
        )

    }

}

除非我再次使用除了

adapter.submitList(item)
标志之外的同一组项目和内容再次调用
isSelected
,否则效果很好。这里发生的情况是,屏幕上可见的第一组项目的
setOnClickListener
没有更新,并且仍然指向旧的一组项目。

假设我们有

RecyclerView
,且
GridLayoutManager
跨度计数为 3。

// First Row
BTC(isSelected = true) // default
ETH(isSelected = false)
BNB(isSelected = false)

// Second Row
DAI(isSelected = false)
USDT(isSelected = false) 
SOL(isSelected = false)

// Third Row
ADA(isSelected = false)
XRP(isSelected = false)
DOGE(isSelected = false)

BTC、ETH、BNB、DAI、USDT、SOL在屏幕上可见,而ADA、XRP、DOGE略微可见。

选择 ETH、DAI、USDT 可以正常工作,因为 CardView 已被标记并且点击侦听器指向正确的项目和正确的数据。

// First Row
BTC(isSelected = true) // default
ETH(isSelected = true)
BNB(isSelected = false)

// Second Row
DAI(isSelected = true)
USDT(isSelected = true) 
SOL(isSelected = false)

// Third Row
ADA(isSelected = false)
XRP(isSelected = false)
DOGE(isSelected = false)

如果我们使用相同数量的项目和内容调用

adapter.submitList(items)
,当然除了
isSelected
标志,该标志将再次设置为 false,因为这是默认状态。它将刷新
RecyclerView
并将所有选中标记状态(isSelected)更新为默认值,这再次正确。

// First Row
BTC(isSelected = true) // default
ETH(isSelected = false)
BNB(isSelected = false)

// Second Row
DAI(isSelected = false)
USDT(isSelected = false) 
SOL(isSelected = false)

// Third Row
ADA(isSelected = false)
XRP(isSelected = false)
DOGE(isSelected = false)

但是,如果您选择 BNB、SOL、ADA、XRP 和 DOGE,点击监听器仍指向数据集的旧参考,同时再次选择 ETH、DAI、USDT 工作正常。

// First Row
BTC (isSelected = true) // default
ETH (isSelected = false) // working as expected
BNB(isSelected = true but pointing to old reference)

// Second Row
DAI(isSelected = false) // working as expected
USDT(isSelected = false) // working as expected
SOL(isSelected = true but pointing to old reference)

// Third Row
ADA(isSelected = true but pointing to old reference)
XRP (isSelected = true but pointing to old reference)
DOGE (isSelected = true but pointing to old reference)

我发现

override fun onBindViewHolder(holder: ItemView, position: Int, payloads: List<Any>)
只对以下尚不可见的项目进行完全绑定,并对 ETH、DAI、USDT 进行部分绑定(这是 DiffUtil
getChangePayload
的工作,很可能是它具有没问题),但跳过之前未接触过的 BTC、BNB、SOL、ADA、XRP 和 DOGE 的重新绑定。

我的问题是,如果适配器的

onBindViewHolder
跳过重新绑定这些项目,如何在此处正确执行单击侦听器的绑定?对于框架来说,即使是部分重新绑定也没有必要,因为这些项目的数据没有任何变化,但这样做会给我们留下一个过时的
setOnClickListener

android android-recyclerview listadapter android-diffutils android-listadapter
1个回答
0
投票

终于找到了解决这个问题的方法,您所要做的就是将点击逻辑移动到保存 RecyclerView 及其适配器的 Fragment 或 Activity 中。

adapter = AssetMinAdapter(
                    AppConfig.remote.iconLink,
                    glide,
                    object : AssetMinAdapter.ItemListener {

                    override fun onAssetSelected(position: Int, cardView: MaterialCardView) {

                        val domain = adapter.currentList[position]
                        if (domain.name.equals(AssetMinDataDomain.DEFAULT_ASSET, true))
                            return

                        val selectedSize = adapter.currentList.filter { it.isSelected }.size

                        // To avoid abuse usage, limit the selectable asset market cap
                        // this also including the default asset which is BTC
                        if (selectedSize >= 6 && !domain.isSelected) {
                            showToast(getString(R.string.max_size_reach), Toast.LENGTH_LONG)
                            return
                        }

                        domain.isSelected = !cardView.isChecked
                        cardView.isChecked = domain.isSelected
                        
                        ...

然后只需使用接口进行回调,这样您就不会受限于过时的适配器的

currentList
getItem

// Upon scroll we need to rebind click listener regardless if full or partial update
                // this is to ensure that click listener is bound to correct item.
                cardView.setOnClickListener {
    
                    itemListener.onAssetSelected(bindingAdapterPosition, cardView)
    
                }

有了这个,我什至可以将点击监听器移动到完全绑定,只有确信它的事件将始终访问最新的数据集。

© www.soinside.com 2019 - 2024. All rights reserved.