Search code examples
androidkotlinandroid-recyclerviewandroid-roomkotlin-coroutines

my app crashes "sometimes" when i press delete button


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>

Solution

  • 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).