1

so im building to do list application for studying purpose and so far i've made recyclerview, adding new item. i'm now trying to implement multi select for recyclerview items selecting and deleting them after selecting multiple of them. items are saved in room db. im using selection tracker library to get selected recyclerview items IDs, then i start deleting items with those IDs.

now problem is, when i launch this app, this delete button does work sometimes, but after trying to delete some more items, it crashes. it needs relaunching of app to work again and even then, it doesnt always work. i've been trying to find fix to it but so far havent found any. it would be much appreciated if anyone can give me any directions.image of my app. delete button is up in toolbar im also pretty new to android development and in case you need any other piece of my code, feel free to ask.

stack trace:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.todolist, PID: 9898
    java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 0(offset:-1).state:6 androidx.recyclerview.widget.RecyclerView{737e557 VFED..... ......ID 0,154-1080,1396 #7f080207 app:id/toDoRecyclerView}, adapter:com.example.todolist.Adapters.ToDoAdapter@2f75844, layout:androidx.recyclerview.widget.LinearLayoutManager@8cd7d2d, context:com.example.todolist.Activity.MainActivity@50574a3
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6183)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
        at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
        at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
        at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
        at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:675)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:4085)
        at androidx.recyclerview.widget.RecyclerView.onMeasure(RecyclerView.java:3534)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
        at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
        at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at androidx.appcompat.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:145)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
        at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
        at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
        at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
        at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6957)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at com.android.internal.policy.DecorView.onMeasure(DecorView.java:747)
        at android.view.View.measure(View.java:25466)
        at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:3397)
        at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:2228)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2486)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1952)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:8171)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:972)
        at android.view.Choreographer.doCallbacks(Choreographer.java:796)
        at android.view.Choreographer.doFrame(Choreographer.java:731)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

here's my Dao:

interface NoteDao {
    @Query("select * from notes")
    fun getNotes() : LiveData<List<RoomNote>>

    @Query("select * from notes where id = :id")
    suspend fun getNoteById(id: Int) :RoomNote

    @Delete
    suspend fun deleteNote(note: RoomNote)

    @Query("delete from notes where id = :id")
    suspend fun deleteSingleItem(id: Int)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertOrUpdateNote(note: RoomNote)
}

data class:

@Entity(
    tableName = "notes"
)
data class RoomNote(
    @ColumnInfo(name = "creation_date")
    val creationDate: String,
    @ColumnInfo(name = "title")
    val title: String,
    @ColumnInfo(name = "contents")
    val contents: String,
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id")
    val id: Int
)

view model:

class ToDoViewModel(private val repository: ToDoRepository) :ViewModel(){

    //get all notes
    val allNotes : LiveData<List<RoomNote>> = repository.allNotes


    //add new item
    suspend fun insert(note: RoomNote)  = viewModelScope.launch{
        repository.insert(note)
    }

    //get single note
    fun getSingleNote(id: Int) = viewModelScope.launch{
        repository.getSingleNote(id)
    }
    // delete note
    suspend fun deleteNote(note: RoomNote) = viewModelScope.launch {
        repository.deleteNote(note)
    }
    suspend fun deleteSingleItem(id: Int) = viewModelScope.launch(Dispatchers.IO) {
        repository.deleteSingleItem(id)
    }

}

class ToDoViewModelFactory(private val repository: ToDoRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(ToDoViewModel::class.java)) {

            return ToDoViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

repository:

class ToDoRepository(private val noteDao: NoteDao) {

    val allNotes : LiveData<List<RoomNote>> = noteDao.getNotes()

    @WorkerThread
    suspend fun insert(note: RoomNote){
        noteDao.insertOrUpdateNote(note)
    }

    @WorkerThread
    suspend fun getSingleNote(id: Int) {
        noteDao.getNoteById(id)
    }
    suspend fun deleteNote(note: RoomNote){
        noteDao.deleteNote(note)
    }
    suspend fun deleteSingleItem(id: Int){
        noteDao.deleteSingleItem(id)
    }
}

activity:

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: ToDoAdapter
    private var tracker: SelectionTracker<Long>? = null
    var itemsSelected = mutableListOf<Long>()

    private val listViewModel:ToDoViewModel by viewModels{
        ToDoViewModelFactory((application as ToDoApplication).repository)
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
        listViewModel.allNotes.observe(this, { newData ->
            adapter.submitList(newData)
        })
        setSupportActionBar(binding.appBar)
        init()
        trackSelectedItems()
    }


    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        binding.appBar.inflateMenu(R.menu.main_activity_toolbar_menu)
        return super.onCreateOptionsMenu(menu)
    }


    override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
        R.id.addNewItem -> {
            val intent = Intent(this, AddNewItem::class.java)
            startActivity(intent)
            true
        }
        R.id.deleteItemsButton ->{
            launchDeletion()
            d("clicked","clicked")
            true
        }
        else -> {
            super.onOptionsItemSelected(item)
        }
    }


    private fun init() {
        adapter = ToDoAdapter()
        binding.toDoRecyclerView.layoutManager = LinearLayoutManager(this)
        binding.toDoRecyclerView.adapter = adapter

    }
    private fun trackSelectedItems() {
        tracker = SelectionTracker.Builder<Long>(
            "selection-1",
            binding.toDoRecyclerView,
            StableIdKeyProvider(binding.toDoRecyclerView),
            ItemLookup(binding.toDoRecyclerView),
            StorageStrategy.createLongStorage()
        ).withSelectionPredicate(SelectionPredicates.createSelectAnything())
            .build()

        adapter.setTracker(tracker)

        tracker?.addObserver(object: SelectionTracker.SelectionObserver<Long>() {
            override fun onSelectionChanged() {
                //handle the selected according to your logic
                itemsSelected.clear()
                itemsSelected.addAll(tracker!!.selection)
                d("itemsSelected","$itemsSelected}")
            }
        })
    }
    private fun launchDeletion() = runBlocking {
        launch {
            itemsSelectedDeletion() }
        }

    private suspend fun itemsSelectedDeletion(){
        for (i in itemsSelected){
            listViewModel.deleteSingleItem(i.toInt())
        }
    }

    inner class ItemLookup(private val rv: RecyclerView)
        : ItemDetailsLookup<Long>() {
        override fun getItemDetails(event: MotionEvent)
                : ItemDetails<Long>? {
            val view = rv.findChildViewUnder(event.x, event.y)
            if(view != null) {
                return (rv.getChildViewHolder(view) as ViewHolder).getItemDetails()
            }
            return null
        }
    }

}

adapter

class ToDoAdapter: ListAdapter<RoomNote, ViewHolder>(WordsComparator()) {

    private var tracker: SelectionTracker<Long>? = null

    fun setTracker(tracker: SelectionTracker<Long>?) {
        this.tracker = tracker
    }
    init {
        setHasStableIds(true)
    }

    override fun getItemId(position: Int): Long = position.toLong()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.create(parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val current = getItem(position)
        holder.title.text = current.title
        holder.creationDate.text = current.creationDate

        tracker?.let {
            if (it.isSelected(position.toLong())) {
                it.select(position.toLong())
                holder.marked.alpha = 1.0F
            } else {
                it.deselect(position.toLong())
                holder.marked.alpha = 0.0F
            }
        }
    }

}

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {

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

    val title: TextView = itemView.findViewById(R.id.title)
    val creationDate: TextView = itemView.findViewById(R.id.creationDate)
    val marked: Button = itemView.findViewById(R.id.checkButton)

    companion object {
        fun create(parent: ViewGroup): ViewHolder {
            val view: View = LayoutInflater.from(parent.context)
                .inflate(R.layout.todo_item, parent, false)
            return ViewHolder(view)
        }
    }


}


class WordsComparator : DiffUtil.ItemCallback<RoomNote>() {
    override fun areContentsTheSame(oldItem: RoomNote, newItem: RoomNote): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areItemsTheSame(oldItem: RoomNote, newItem: RoomNote): Boolean {
        return oldItem == newItem
    }

}

toolbar menu:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/deleteItemsButton"
        android:icon="@mipmap/delete_button"
        android:title="plus"
        app:showAsAction="always"
        />

    <item
        android:id="@+id/addNewItem"
        android:icon="@mipmap/plus_icon"
        android:title="plus"
        app:showAsAction="always"
    />

</menu>
xRawAnger
  • 13
  • 3
  • i did look thru stack trace and it pointed towards recyclerview. i found some answers to problem similar to mine but im not exactly sure where to use those answers in my MVVM architecture. https://stackoverflow.com/questions/30220771/recyclerview-inconsistency-detected-invalid-item-position – xRawAnger Jan 17 '22 at 11:57

1 Answers1

1

In your adapter you have:

setHasStableIds(true)

and

override fun getItemId(position: Int): Long = position.toLong()

That's conflicting. When items are deleted, positions and therefore ids change, making the ids to not be stable.

Either remove the setHasStableIds(true) or come up with a method for producing actually stable item ids (such as based on the id field in the data).

laalto
  • 150,114
  • 66
  • 286
  • 303
  • thx for your reply. seems like i'll have to find a way to make custom item key provider – xRawAnger Jan 17 '22 at 16:09
  • @xRawAnger can't you just use ``getItem(position).id``? That is literally the item's ID, and it should be stable and unique (since it's the autogenerated primary key in the DB) – cactustictacs Jan 17 '22 at 22:24
  • @cactustictacs thanks for your suggestion. it seem to have made ids stable but now another portion of my code seems to have been broken xd. in my onBindViewholder i had code that put check marks on chosen items, now it doesnt seem to be working. can you please give any references as to why it doesnt work anymore?(as i mentioned im pretty new to kotlin) – xRawAnger Jan 19 '22 at 09:38
  • @xRawAnger you should post a new question about it. I haven't used ``SelectionTracker`` so I don't know, sorry! – cactustictacs Jan 19 '22 at 19:37