【问题标题】:How to fix IndexOutOfBoundsException in RecyclerView (Jetpack Navigation, DataBinding)如何修复 RecyclerView 中的 IndexOutOfBoundsException(Jetpack Navigation、DataBinding)
【发布时间】:2021-12-08 11:00:02
【问题描述】:

我的应用使用“Jetpack Navigation、DataBinding、ViewModel”。

一个 Fragment 有 RecyclerView。

如果我触摸其中一个项目,它会导航到 B 片段。

当我回到 A 片段(通过后退按钮)时,会发生崩溃。

IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter

片段是:

@AndroidEntryPoint
class AFragment : Fragment() {

    private var _binding: FragmentABinding? = null
    private val binding: FragmentABinding
        get() = _binding!!

    private val viewModel: AViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentABinding.inflate(inflater, container, false)
        binding.vm = viewModel
        binding.lifecycleOwner = viewLifecycleOwner
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.rvItems.adapter = ItemsAdapter()
        viewModel.getItems()
    }

}

ViewModel 是:

@HiltViewModel
class AViewModel @Inject constructor(
    private val itemRepo: ItemRepo,
    private val disposable: CompositeDisposable
) : ViewModel() {

    private var _items: MutableLiveData<List<Item>> = MutableLiveData()
    val items: LiveData<List<Item>>
        get() = _items

    override fun onCleared() {
        super.onCleared()
        disposable.clear()
    }

    fun getItems() {
        itemRepo.getAll()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                _items.postValue(it)
            }, { t ->

            })
            .addTo(disposable)
    }
}

XML 布局是:

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_items"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:bind_items="@{vm.items}"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

绑定适配器是:

@BindingAdapter("bind_items")
fun setItems(view: RecyclerView, list: List<Item>?) {
    if (!list.isNullOrEmpty()) {
        (view.adapter as? ItemsAdapter)?.update(list)
    }
}

ItemsAdapter 是:

class ItemsAdapter : RecyclerView.Adapter<ItemsAdapter.ItemHolder>() {

    private val items: MutableList<Item> = mutableListOf()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder {
        return ItemHolder (ItemItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: ItemHolder, position: Int) {
        holder.bind(items[position])
    }

    fun update(newItems: List<Item>) {
        items.clear()
        items.addAll(newItems)
        notifyItemRangeInserted(0, newItems.size)
    }

    class ItemHolder(
        private val binding: ItemItemBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(item: Item) {
            with(binding) {
                tvDate.text = item.date
            }
        }
    }
}

我认为这个问题与“Jetpack Navigation”有关。

但我不知道我该怎么做...

【问题讨论】:

  • ItemsAdapter.update 是做什么的?
  • 我也添加了适配器代码
  • 这个notifyItemInsertRange(0, newItems.size) 方法有什么作用?据我所知,这不是标准方法 - 这:Adapter::notifyItemRangeInserted(int positionStart, int itemCount)Adapter::notifyItemRangeChanged(int positionStart, int itemCount) 是您通常调用的方法。考虑使用 DiffUtil 类,甚至是使用 AsyncListDifferListAdapter 来计算和应用更改。
  • 只是打错了...我用notifyItemRangeInserted对。

标签: android android-recyclerview android-databinding android-jetpack-navigation


【解决方案1】:

删除项目时您不会告诉适配器,您必须执行以下操作:

fun update(newItems: List<Item>) {

    // remove the old items
    val originalSize = items.size
    items.clear()
    notifyItemRangeRemoved(0, originalSize)

    // add the new items
    items.addAll(newItems)
    notifyItemRangeInserted(0, newItems.size)
}

但这看起来有点奇怪,我怀疑这就是你想要的(它会为旧项目设置动画,然后为新项目设置动画)。就像之前建议的答案一样,最简单的方法是使用 notifyDataSetChanged 代替(列表将立即在视觉上改变,没有动画)

fun update(newItems: List<Item>) {
    items.clear()
    items.addAll(newItems)
    notifyDataSetChanged()
}

如果您想要漂亮的更改动画,您可能应该查看 DiffUtil(这将计算您的两个列表之间的差异 - 但对于大型列表来说这是一个昂贵的调用)。你可以在 UI 线程上运行小列表,但你不应该这样做(如果你想保证健壮的代码,在 UI 线程上使用 DiffUtil 会更复杂一些)。你可以像这样得到 DiffResult:

fun createDiffResult(oldList: List<Thing>, newList: List<Thing>): DiffUtil.DiffResult {

    return DiffUtil.calculateDiff(object : DiffUtil.Callback() {

        override fun getOldListSize(): Int {
            return oldList.size
        }

        override fun getNewListSize(): Int {
            return newList.size
        }

        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {

            // are the items conceptually the same item?
            // (you might compare a unique id here)

            return true
        }

        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {

            // do the items _look_ the same when rendered in your recycler view?
            // (you only need to compare the values for the things that are visible in your implementation of the item view)

            return true
        }
    })
}

然后像这样使用它:

diffResult.dispatchUpdatesTo(adapter);

【讨论】:

    【解决方案2】:

    来自RecyclerView.AdapternotifyItemRangeInserted的android文档:

    通知任何已注册的观察者当前反映的 itemCount 项 > 从 positionStart 开始已新插入。以前位于 >positionStart 及以后的项目现在可以从位置 positionStart + >itemCount 开始找到。

    这意味着在您调用该函数后,适配器期望列表为 oldList.size + newList.size,但只找到新元素,因此超出范围。

    最简单的解决方案是改用notifyDataSetChanged,但您可能希望使用其他解决方案来提高性能。

    【讨论】:

      猜你喜欢
      • 2018-06-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多