user128440
user128440

Reputation: 376

RecyclerView SelectionTracker IndexOutOfBoundsException when removing first item

I try to use selection tracker for multi-selection with contextual action mode and got stuck at deleting first item.
I've set up tracker like in this tutorial, selection and removing other items works, but when I remove first item I get this exception:
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 0(offset:-1).state:5
Without setHasStableIds and overridden getItemId method in adapter deletion works fine, but when i select item i get IllegalArgumentException.
I found almost identical issue here but solution didn't help me.
At the moment I use version 1.1.0 of recyclerview selection library.
Here is my adapter:

class WordAdapter(private val onWordClicked: (Word) -> Unit) : ListAdapter<Word, WordAdapter.WordViewHolder>(DiffCallback) {
    var tracker: SelectionTracker<Long>? = null

    init {
        setHasStableIds(true)
    }

    override fun getItemId(position: Int): Long {
        //return getItem(position).wordId.toLong()
        position.toLong()
    }

    /*override fun getItemViewType(position: Int): Int {
        return position
    }*/

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

    override fun onBindViewHolder(holder: WordAdapter.WordViewHolder, position: Int) {
        val current = getItem(position)

        holder.itemView.setOnClickListener {
            onWordClicked(current)
        }

        tracker?.let {
            holder.bind(current, it.isSelected(position.toLong()))
        }
    }

    class WordViewHolder(private var binding: ItemWordBinding) : RecyclerView.ViewHolder(binding.root) {

        fun bind(word: Word, isActivated: Boolean = false) {
            binding.word.text = word.word
            binding.translation.text = word.translation
            (itemView as MaterialCardView).isChecked = isActivated
        }

        fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> = object : ItemDetailsLookup.ItemDetails<Long>() {
            override fun getPosition(): Int = adapterPosition

            override fun getSelectionKey(): Long? = itemId
        }
    }

    companion object {
        private val DiffCallback = object : DiffUtil.ItemCallback<Word>() {
            override fun areItemsTheSame(oldWord: Word, newWord: Word): Boolean {
                return oldWord.wordId == newWord.wordId
            }

            override fun areContentsTheSame(oldWord: Word, newWord: Word): Boolean {
                return oldWord.word == newWord.word
            }
        }
    }
}

My fragment:

class WordsFragment : Fragment() {

    private val viewModel: WordsViewModel by viewModels() {
        WordViewModelFactory(
            (activity?.application as DictionaryApplication).database.wordDao()
        )
    }

    private var _binding: FragmentWordsBinding? = null

    private val binding get() = _binding!!

    private val navigationArgs: WordsFragmentArgs by navArgs()

    private var topicId: Int? = null

    var actionMode: ActionMode? = null

    private var tracker: SelectionTracker<Long>? = null

    private val actionModeCallback = object : ActionMode.Callback {

        override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
            mode!!.menuInflater.inflate(R.menu.menu_contextual_action_bar, menu)
            return true
        }

        override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
            return false
        }

        override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
            return when (item!!.itemId) {
                R.id.action_delete_word -> {
                    val list = tracker!!.selection.map {
                        (binding.wordsRecyclerView.adapter!! as WordAdapter?)!!.currentList[it.toInt()]
                    }.toList()
                    tracker!!.clearSelection()
                    for (word in list) {
                        viewModel.deleteWord(word)
                    }
                    Toast.makeText(activity!!, "Delete button pressed", Toast.LENGTH_SHORT).show()
                    mode!!.finish()
                    true
                }
                else -> false
            }
        }

        override fun onDestroyActionMode(mode: ActionMode?) {
            actionMode = null
            tracker?.clearSelection()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        _binding = FragmentWordsBinding.inflate(inflater, container, false)
        return binding.root
    }

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

        topicId = navigationArgs.topicId

        val adapter = WordAdapter { word ->
            AddWordDialogFragment { dialogBinding ->
                viewModel.updateWord(
                    dialogBinding.wordEditText.toString(),
                    dialogBinding.translationEditText.toString(),
                    word.topicId
                )
            }.show(requireActivity().supportFragmentManager, "addingWord")
        }

        binding.wordsRecyclerView.layoutManager = LinearLayoutManager(this.context)
        binding.wordsRecyclerView.adapter = adapter

        viewModel.allWords.observe(this.viewLifecycleOwner) {
            adapter.submitList(it)
        }

        tracker = SelectionTracker.Builder(
            "wordSelection",
            binding.wordsRecyclerView,
            StableIdKeyProvider(binding.wordsRecyclerView),
            //WordItemKeyProvider(binding.wordsRecyclerView),
            WordDetailsLookup(binding.wordsRecyclerView),
            StorageStrategy.createLongStorage()
        ).withSelectionPredicate(SelectionPredicates.createSelectAnything()).build()

        adapter.tracker = tracker

        tracker!!.addObserver(object : SelectionTracker.SelectionObserver<Long>() {
            override fun onSelectionChanged() {
                super.onSelectionChanged()
                if (tracker!!.hasSelection() && actionMode == null) {
                    actionMode = (activity as MainActivity?)!!.startSupportActionMode(actionModeCallback)
                    setMenuItemTitle(tracker!!.selection.size())
                } else if (!tracker!!.hasSelection() && actionMode != null) {
                    actionMode!!.finish();
                    actionMode = null;
                } else {
                    setMenuItemTitle(tracker!!.selection.size());
                }
            }
        })
    }

    private fun setMenuItemTitle(selectedItemSize: Int) {
        actionMode?.title = getString(
            R.string.action_mode_title,
            resources.getQuantityString(R.plurals.words, selectedItemSize, selectedItemSize)
        )
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.menu_main, menu)
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        return when (item.itemId) {
            R.id.action_add_word -> {
                AddWordDialogFragment { dialogBinding ->
                    dialogBinding.let {
                        viewModel.addNewWord(
                            it.wordEditText.text.toString(),
                            it.translationEditText.text.toString(),
                            topicId!!
                        )
                    }
                }.show(requireActivity().supportFragmentManager, "wordAdding")
                true
            }
            else -> super.onOptionsItemSelected(item)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

ItemDetailsLookup:

class WordDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<Long>() {
    override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
        val view = recyclerView.findChildViewUnder(e.x, e.y)
        if (view != null) {
            return (recyclerView.getChildViewHolder(view) as WordAdapter.WordViewHolder).getItemDetails()
        }
        return null
    }
}

ItemKeyProvider:

class WordItemKeyProvider(private val recyclerView: RecyclerView) :
    ItemKeyProvider<Long>(ItemKeyProvider.SCOPE_MAPPED) {
    override fun getKey(position: Int): Long? {
        return recyclerView.adapter?.getItemId(position)
        //return (recyclerView.adapter as WordAdapter).currentList[position].wordId.toLong()
    }

    override fun getPosition(key: Long): Int {
        val viewHolder = recyclerView.findViewHolderForItemId(key)
        return viewHolder?.layoutPosition ?: RecyclerView.NO_POSITION
        //return (recyclerView.adapter as WordAdapter).currentList.indexOfFirst { it.wordId.toLong() == key }
    }
}

Upvotes: 1

Views: 250

Answers (0)

Related Questions