K.R.
K.R.

Reputation: 466

ArrayIndexOutOfBoundsException when adding to ArrayList in ListView

I've found several other questions about this exception and none of them have an answer that works for me.

Here is my entire code:

class MainActivity : AppCompatActivity() {
    private lateinit var listView: ListView
    private lateinit var addItem: ImageButton
    private lateinit var adapter: MyAdapter

    private val names = arrayListOf("Person 1", "Person 2", "Person 3", "Person 4", "Person 5",
        "Person 6", "Person 7", "Person 8", "Person 9", "Person 10")
    private val descriptions = arrayListOf("Actor", "Activist", "Athlete", "Scientist", "Astronaut",
        "Engineer", "Software Developer", "Politician", "Soldier", "Shopkeeper")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        listView = list_view as ListView
        addItem = addButton as ImageButton
        adapter = MyAdapter(this, names, descriptions)
        listView.adapter = adapter
        addItem.setOnClickListener {
            names.add("Person 11")
            descriptions.add("Mortician")
            adapter.notifyDataSetChanged()
            Toast.makeText(this, "Button Clicked", Toast.LENGTH_LONG).show()
        }
    }
}

private class MyAdapter(val context: Context, val names: ArrayList<String>,
            val descriptions: ArrayList<String>) : BaseAdapter() {
    override fun getCount(): Int {
        return names.size
    }

    override fun getViewTypeCount(): Int {
        return count
    }

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

    override fun getItem(position: Int): Any {
        return position
    }

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

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val viewHolder: ViewHolder?

        return if (convertView == null) {
            val convertViewMutable = LayoutInflater.from(context).inflate(R.layout.activity_custom, parent, false)

            val checkBox: CheckBox = convertViewMutable.findViewById(R.id.checkBox_name) as CheckBox
            val desc: TextView = convertViewMutable.findViewById(R.id.textView_desc) as TextView

            viewHolder = ViewHolder(checkBox, desc)
            convertViewMutable.tag = viewHolder

            checkBox.text = names[position]
            desc.text = descriptions[position]

            convertViewMutable
        } else
            convertView
    }
}
private data class ViewHolder(private val checkBox: CheckBox, private val textView: TextView)

When I run the program, the list appears fine. When I click the button to add the new name and description it runs all the code in the listener since I see the Toast appear. I can then scroll down and see the new item at the bottom of the list. But, when I scroll back up the error is thrown.

I saw an answer for someone saying you shouldn't override getViewTypeCount() and getItemViewType() but I am only overriding them because if I take them out the items in my list will duplicate when I scroll through them. There were also answers saying the error happens because getItemViewType() is returning a number greater than getViewTypeCount() but they don't mention how to fix it.

Upvotes: 0

Views: 338

Answers (2)

Hong Duan
Hong Duan

Reputation: 4304

As @Jackey said, you have some mistakes when implementing the ListAdapter.

To make the ListView reuse the ViewHolders properly, you need to figure out two things:

  1. how many different items in your list? (in your case, you only need 1)
  2. what is the type of each item at a position? (in your case, only 1 type, so every item has the same type)

So in your case, you need not override these two methods, because the default implementation of BaseAdapter is:

public int getItemViewType(int position) {
    return 0; // type is 0 for every item
}

public int getViewTypeCount() {
    return 1; // we only have 1 type
}

I'll give you a working example to help you to understand this:

class MyAdapter(val context: Context, val names: ArrayList<String>,
                        val descriptions: ArrayList<String>) : BaseAdapter() {
    override fun getCount(): Int {
        return names.size
    }

    override fun getItem(position: Int): Any {
        return position
    }

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

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val viewHolder: ViewHolder

        var convertViewMutable = convertView
        if (convertView == null) {
            convertViewMutable = LayoutInflater.from(context).inflate(R.layout.activity_custom, parent, false)

            val checkBox: CheckBox = convertViewMutable.findViewById(R.id.checkBox_name) as CheckBox
            val desc: TextView = convertViewMutable.findViewById(R.id.textView_desc) as TextView

            viewHolder = ViewHolder(checkBox, desc)
            convertViewMutable.tag = viewHolder
        } else {
            viewHolder = convertViewMutable!!.tag as ViewHolder
        }

        viewHolder.checkBox.text = names[position]
        viewHolder.textView.text = descriptions[position]

        return convertViewMutable!!
    }
}

private data class ViewHolder(val checkBox: CheckBox, val textView: TextView)

Upvotes: 0

Jackey
Jackey

Reputation: 3234

This is happening because you're using ViewHolder improperly.

But before I explain the problem, I want to tell you your way of overriding getViewTypeCount() and getItemViewType() is a serious mistake.

The point of a ViewHolder is so it can hold onto a view for the ListView to reuse. This way, ListView doesn't have to keep recreating a new view for each item in the ArrayList.

However, you're doing the complete opposite with your code. By how you're overriding getViewTypeCount() and getItemViewType(), you're essentially destroying the purpose of ViewHolders. You're telling the ListView to create a new ViewHolder for each item in the ArrayList, which makes it not be able to reuse the views stored in the ViewHolder.

The reason why not overriding those two methods will cause the items to duplicate is related to the solution of your question, which is using ViewHolder's properly.

To use ViewHolder's properly, when you get to the code that says:

} else
        convertView

Don't just return the convertView. Get the ViewHolder from the convertView's TAG and update the CheckBox and TextView with the current item's data.

This way, your ViewHolder will always hold the name and description it's meant to hold.

You will also be reusing ViewHolder's properly and not creating a new one for each item.

The reason why your previous code was duplicating the items while you scroll is because you didn't update the ViewHolder with proper data, so it was just using the old data from previous items.

So to summarize:

  1. Do not override getViewTypeCount() and getItemViewType()

  2. Have your getView() method update the ViewHolder's views with the proper data.

Upvotes: 3

Related Questions