Reputation: 376
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