Как исправить исключение IndexOutOfBoundsException в RecyclerView (навигация по реактивному ранцу, привязка данных)

#android #android-recyclerview #android-databinding #android-jetpack-navigation

Вопрос:

Мое приложение использует «Навигацию по реактивному ранцу, привязку данных, модель просмотра».

Фрагмент имеет возможность повторного просмотра.

Если я коснусь одного из элементов, он перейдет к фрагменту B.

И когда я возвращаюсь к фрагменту (кнопкой «Назад»), происходит сбой.

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

Модель представления-это:

 @HiltViewModel class AViewModel @Inject constructor(  private val itemRepo: ItemRepo,  private val disposable: CompositeDisposable ) : ViewModel() {   private var _items: MutableLiveDatalt;Listlt;Itemgt;gt; = MutableLiveData()  val items: LiveDatalt;Listlt;Itemgt;gt;  get() = _items   override fun onCleared() {  super.onCleared()  disposable.clear()  }   fun getItems() {  itemRepo.getAll()  .subscribeOn(Schedulers.io())  .observeOn(AndroidSchedulers.mainThread())  .subscribe({  _items.postValue(it)  }, { t -gt;   })  .addTo(disposable)  } }  

XML Layout is:

 lt;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" /gt;  

Binding Adapter is:

 @BindingAdapter("bind_items") fun setItems(view: RecyclerView, list: Listlt;Itemgt;?) {  if (!list.isNullOrEmpty()) {  (view.adapter as? ItemsAdapter)?.update(list)  } }  

Адаптером элементов является:

 class ItemsAdapter : RecyclerView.Adapterlt;ItemsAdapter.ItemHoldergt;() {   private val items: MutableListlt;Itemgt; = 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: Listlt;Itemgt;) {  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  }  }  } }  

Я думаю, что эта проблема связана с «Навигацией по реактивному ранцу».

Но я точно не знаю, что мне делать…

Комментарии:

1. что же ItemsAdapter.update делать ?

2. Я тоже добавил код адаптера

3. что делает этот notifyItemInsertRange(0, newItems.size) метод? Насколько я вижу, это не стандартный метод — это : Adapter::notifyItemRangeInserted(int positionStart, int itemCount) или Adapter::notifyItemRangeChanged(int positionStart, int itemCount) это метод, который вы обычно вызываете. Подумайте об использовании класса DiffUtil или даже a ListAdapter , который использует a AsyncListDiffer для разработки и применения изменений для вас.

4. Это просто неправильный набор текста… Я использовал notifyItemRangeInserted правильно.

Ответ №1:

Из документации RecyclerView.Adapter для Android о notifyItemRangeInserted :

Уведомите всех зарегистрированных наблюдателей о том, что отображаемые в настоящее время элементы количества элементов gt;начиная с позиции «Начало» были добавлены заново. Элементы, ранее расположенные в gt;Начало позиции и далее, теперь можно найти, начиная с позиции Начало позиции gt;gt;Количество элементов.

Это означает, что после вызова этой функции адаптер ожидает, что список будет длинным oldList.size newList.size, но находит только новые элементы и поэтому выходит за рамки.

Самое простое решение-использовать notifyDataSetChanged вместо этого, хотя для повышения производительности вы можете использовать другие решения.

Ответ №2:

Вы не сообщаете адаптеру, когда удаляете элементы, вам придется сделать что-то вроде этого:

 fun update(newItems: Listlt;Itemgt;) {   // 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: Listlt;Itemgt;) {  items.clear()  items.addAll(newItems)  notifyDataSetChanged() }  

Если вам нужна хорошая анимация изменений, вам, вероятно, следует изучить DiffUtil (это вычислит разницу между вашими двумя списками, но это дорогостоящий вызов для больших списков). Вам может сойти с рук запуск в потоке пользовательского интерфейса для небольших списков, но на самом деле вы не должны этого делать (использование DiffUtil вне потока пользовательского интерфейса немного сложнее, если вы хотите гарантировать надежный код). Вы могли бы получить другой результат, как это:

 fun createDiffResult(oldList: Listlt;Thinggt;, newList: Listlt;Thinggt;): 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);