Search code examples
androidkotlinandroid-arrayadapterautocompletetextview

How to make custom Filter for AutoCompleteTextView using custom Adapter?


I have an AutoCompleteTextView in my layout which will get data from an API, the data returns two params, description and code, the AutoCompleteText has to be filterable for both code and description.

To view both code and description i've made a custom ArrayAdapter and i was trying to implement the Filterable method to set the custom filter which will check for both code and description properties.

The issue is that once i try to write in AutoCompleteTextView my app crash by casting an exception at the start of the publishResults: java.lang.UnsupportedOperationException

So how could i implement a custom filterable to my ArrayAdapter?

Here is my Adapter code:

class FornitoriAdapter(context: Context, resource: Int, private val fornitori: List<Fornitori>) :
    ArrayAdapter<Fornitori>(context, resource, fornitori), Filterable {

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

        if (convertView == null) {
            val inflater = LayoutInflater.from(context)
            row = inflater.inflate(R.layout.list_item, parent, false)
            holder = ViewHolder(row)
            row.tag = holder
        } else {
            row = convertView
            holder = row.tag as ViewHolder
        }

        val fornitore: Fornitori = fornitori[position]

        holder.descrText.text = fornitore.desc
        holder.codeText.text = fornitore.cod

        return row
    }

    override fun getItem(position: Int): Fornitori {
        return fornitori[position]
    }

    override fun getCount(): Int {
        return fornitori.size
    }

    private class ViewHolder constructor(row: View) {
        val descrText: TextView = row.findViewById(R.id.description)
        val codeText: TextView = row.findViewById(R.id.code)

    }

    override fun getFilter(): Filter {
        return customFilter
    }

    private val customFilter = object : Filter() {
    override fun convertResultToString(resultValue: Any?): CharSequence {
        return (resultValue as Fornitori).desc
    }

        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val filteredList = mutableListOf<Fornitori>()
            if (constraint == null || constraint.isEmpty()) {
                filteredList.addAll(fornitori)
            } else {
                for (item in fornitori) {
                    if (item.cod.equals(constraint.toString(), ignoreCase = true) || item.desc.toLowerCase(
                            Locale.ROOT
                        ).startsWith(
                            constraint.toString().toLowerCase(
                                Locale.ROOT
                            )
                        )
                    ) {
                        filteredList.add(item)
                    }
                }
            }
            val results = FilterResults()
            results.values = filteredList
            return results
        }

        override fun publishResults(constraint: CharSequence?, filterResults: FilterResults?) {
            clear()
            val newValues: List<Fornitori> = filterResults?.values as List<Fornitori>
            for (element in newValues) {
                add(element)
            }
            if (filterResults.count > 0) {
                notifyDataSetChanged()
            } else {
                notifyDataSetInvalidated()
            }
        }

    }

}

And here how i manage it in my Fragment:

autocompleteFornitore = view.findViewById(R.id.autocompleteFornitore)

val fornitori: List<Fornitori> = listOf(Fornitori("001", "GAB"), Fornitori("002", "TEST"))
val adapter = FornitoriAdapter(requireContext(), R.layout.list_item, fornitori)
autocompleteFornitore.setAdapter(adapter)


autocompleteFornitore.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
    override fun onItemSelected(
        parent: AdapterView<*>?,
        view: View?,
        position: Int,
        id: Long
    ) {

    }

    override fun onNothingSelected(parent: AdapterView<*>?) {

    }
}

As required full stack trace:

2021-03-09 16:05:26.915 31076-31076/it.xxx.xxx E/AndroidRuntime: FATAL EXCEPTION: main
    Process: it.xxx.xxx, PID: 31076
    java.lang.UnsupportedOperationException
        at java.util.AbstractList.remove(AbstractList.java:161)
        at java.util.AbstractList$Itr.remove(AbstractList.java:374)
        at java.util.AbstractList.removeRange(AbstractList.java:571)
        at java.util.AbstractList.clear(AbstractList.java:234)
        at android.widget.ArrayAdapter.clear(ArrayAdapter.java:320)
        at it.gabtamagnini.visualstock.adapters.FornitoriAdapter$customFilter$1.publishResults(FornitoriAdapter.kt:88)
        at android.widget.Filter$ResultsHandler.handleMessage(Filter.java:282)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6518)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

Solution

  • It says in the documentation of ArrayAdapter that:

    @throws UnsupportedOperationException if the underlying data collection is immutable

    Therefore you should be passing in a MutableList into the ArrayAdapter if you want to use clear(), either by constructing FornitoriAdapter with one, or converting it with Collection<T>.toMutableList().