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