我对
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
终于找到了解决这个问题的方法,您所要做的就是将点击逻辑移动到保存 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)
}
有了这个,我什至可以将点击监听器移动到完全绑定,只有确信它的事件将始终访问最新的数据集。