ExpandableListView with no children throws indexOutOfBoundsException - android

I have an ExpandableListView where some groups have children and some not, what I need to do is to expand only the groups that have children.
Part of the body array elements is empty and because of that, I'm getting an IndexOutOfBoundsException
class ExpandableInnerCartAdapter(
var context: Context,
var expandableListView: ExpandableListView,
var header: MutableList<Cart>,
val isTerminadoFragment:Boolean
) : BaseExpandableListAdapter() {
val map = SparseBooleanArray()
var body: List<List<String>> = listOf()
override fun getGroup(groupPosition: Int): Cart {
return header[groupPosition]
}
override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean {
return true
}
override fun hasStableIds(): Boolean {
return false
}
fun getCart(): MutableList<Cart> = header
fun getCheckedArray():SparseBooleanArray = map
override fun getGroupView(
groupPosition: Int,
isExpanded: Boolean,
convertView: View?,
parent: ViewGroup?
): View {
var convertView = convertView
if(convertView == null){
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
convertView = inflater.inflate(R.layout.layout_group,null)
}
val item = header[groupPosition]
body = listOf(item.optionList)
expandableListView.expandGroup(groupPosition)
expandableListView.setGroupIndicator(null)
convertView.item_name.text = item.productName
return convertView
}
override fun getChildrenCount(groupPosition: Int): Int {
return body[groupPosition].size
}
override fun getChild(groupPosition: Int, childPosition: Int): Any {
return body[groupPosition][childPosition]
}
override fun getGroupId(groupPosition: Int): Long {
return groupPosition.toLong()
}
override fun getChildView(
groupPosition: Int,
childPosition: Int,
isLastChild: Boolean,
convertView: View?,
parent: ViewGroup?
): View {
var convertView = convertView
if(convertView == null){
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
convertView = inflater.inflate(R.layout.layout_child,null)
}
if(getChildrenCount(groupPosition) > 0){
val title = convertView?.findViewById<TextView>(R.id.tv_title)
title?.text = "OpciĆ³n ${childPosition+1} -> ${getChild(groupPosition,childPosition)}"
}
return convertView!!
}
override fun getChildId(groupPosition: Int, childPosition: Int): Long {
return childPosition.toLong()
}
override fun getGroupCount(): Int {
return header.size
}
}
The error seems like it's happening when no group has children and try to do
expandableListView.expandGroup(groupPosition)
I have tried to fix the issue with an if statement:
if(body.isNotEmpty()){
expandableListView.expandGroup(groupPosition)
}
but this solution does not work.
How do I avoid groups that do not have children?
Thanks

As you use Kotlin you have a lot of useful extension functions at your disposal. One of them is filter for which you can specify condition, of course.
The solution would be to filter out empty arrays from the list you set as the new body value:
body = listOf(item.optionList).filter { it.isNotEmpty() }
Filter function definition can be seen here.

Related

Using ViewBinding on an ArrayAdapter

I'm trying to refactor my app to use ViewBinding. I've gone through all the fragments and activities; however, I have an ArrayAdapter that I'm unsure of the proper convention to use view binding to prevent memory leaks.
What is the proper way to use a viewbinding in an ArrayAdapter?
I have been using this method for fragments:
private var _binding: BINDING_FILE_NAME? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = BINDING_FILE_NAME.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
I call my adapter like so:
var myadapter : MyCustomAdapter = MyCustomAdapter(requireContext(), R.layout.row_autocomplete_item, myListOfStrings())
MyCustomAdapter class
class MyCustomAdapter(ctx: Context, private val layout: Int, private val allItems: List<String>) : ArrayAdapter<String>(ctx, layout, allItems) {
var filteredItems: List<String> = listOf()
override fun getCount(): Int = filteredItems.size
override fun getItem(position: Int): String = filteredItems[position]
#SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(parent.context).inflate(layout, parent, false)
val item = filteredItems[position]
view.apply {
// HERE IS WHERE I AM NEEDING TO BIND THE VIEW
tvName?.text = item
}
return view
}
override fun getFilter(): Filter {
return object : Filter() {
override fun publishResults(charSequence: CharSequence?, filterResults: FilterResults) {
#Suppress("UNCHECKED_CAST")
filteredItems = filterResults.values as List<String>
notifyDataSetChanged()
}
override fun performFiltering(charSequence: CharSequence?): FilterResults {
val queryString = charSequence?.toString()?.lowercase(Locale.ROOT)
val results = FilterResults()
results.values = if (queryString == null || queryString.isEmpty())
allItems
else
allItems.filter {
it.lowercase(Locale.ROOT).contains(queryString)
}
return results
}
}
}
}
I did like this, its working. But Im not sure, whether it is correct way or not
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding: LayoutCustomSpinnerBinding
var row = convertView
if (row == null) {
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
binding = LayoutCustomSpinnerBinding.inflate(inflater, parent, false)
row = binding.root
} else {
binding = LayoutCustomSpinnerBinding.bind(row)
}
binding.txtContent.text = spinnerList[position].ValueData
return row
}
Based on this answer, got this:
If convertView is not null, then bind to it. Inflate otherwise.
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding: MyLayoutBinding =
if (convertView != null) MyLayoutBinding.bind(convertView)
else MyLayoutBinding.inflate(LayoutInflater.from(context), parent, false)
// use binding
val item = getItem(position)
binding.text = item.name
return binding.root
}
class HoursAdapter(private val hoursList: List<HoursItem>)
:RecyclerView.Adapter<HoursAdapter.HoursViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
HoursViewHolder {
val binding = HoursListItemsBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return HoursViewHolder(binding)
}
override fun getItemCount() = hoursList.size
override fun onBindViewHolder(holder: HoursViewHolder, position: Int) {
with(holder){
with(hoursList[position]) {
binding.topLearnerName.text = name
val hours = "$hours learning hours, $country"
binding.topLearnerTime.text = hours
GlideApp.with(holder.itemView.context)
.load(badgeUrl)
.into(binding.topLearnerImage)
holder.itemView.setOnClickListener {
Toast.makeText(holder.itemView.context, hours,
Toast.LENGTH_SHORT).show()
}
}
}
}
inner class HoursViewHolder(val binding: HoursListItemsBinding)
:RecyclerView.ViewHolder(binding.root)
}

getView and getItem in BaseAdapter are never called

First it said that my listView is null. Fixed this. Then i debuged the Adapter and i found out that the getView and getItem are never called.
Here is my Adapter:
class PropertiesAdapter(private val context: Context, private val properties: ArrayList<Property>) : BaseAdapter() {
override fun getCount(): Int {
Log.d("getCount", "${properties.size}")
return properties.size
}
override fun getItem(position: Int): Any {
Log.d("getItem", "${properties[position]}")
return properties[position]
}
override fun getItemId(position: Int): Long {
Log.d("getItemId", "${position.toLong()}")
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
Log.d("getView", "$properties")
val property = properties[position]
val layoutInflater = context.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
val myView = layoutInflater.inflate(R.layout.property, null)
myView.title.text = property.title
myView.descr.text = property.descr
myView.location.text = property.location
myView.price.text = property.price
return myView
}
}
Here is my Log:
D/getCount: 3
D/getCount: 3
D/getItemId: 0
D/getItemId: 0

How to use DataBinding in a ListView?

I'm using databiding into a RecyclerView in my layout like below :
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/topup_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:items="#{viewModel.items}"
tools:listitem="#layout/list_item_content" />
and the list and binding adapters like :
#BindingAdapter("app:items")
fun setItems(listView: RecyclerView, items: List<ListItem>?) {
items?.let {
(listView.adapter as MyListAdapter).submitList(items)
}
}
//------------------
class MyListAdapter() :
ListAdapter<ListItem, ViewHolder>(myListItemDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
class ViewHolder private constructor(private val binding: ListItemContentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ListItem) {
binding.item = item
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = TopUpListItemContentBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class myListItemDiffCallback : DiffUtil.ItemCallback<ListItem>() {
override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean {
return oldItem.label == newItem.label
}
override fun areContentsTheSame(
oldItem: ListItem,
newItem: ListItem
): Boolean {
return oldItem == newItem
}
}
But it's not the same case using a simple ListView. For many reasons, I prefer using a listview and I tried but I don't know how to customize my adapter for databiding like I used in RecycleView.
Below is My adapter for Listview :
class MyListAdapter(context: Context, items: List<ListItem>) :
ArrayAdapter<ListItem>(context, 0, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = ListItemContentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
binding.item = getItem(position)
return binding.root
}
}
My Item class Model :
data class ListItem(
val iconResId: Int,
val label: String,
val action: () -> Unit
)
Any one has a clue ?
Try to update your adapter like below:
class MyListAdapter(context: Context, var items: List<ListItem> = arrayListOf()) :
ArrayAdapter<ListItem>(context, 0, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = ListItemContentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
binding.item = items[position]
return binding.root
}
override fun getCount(): Int {
return items.size
}
fun submitList(items: List<ListItem>) {
this.items = items
notifyDataSetChanged()
}
}

How to get the selected items from dynamic created spinner

I have a spinner that loads dynamic data from server. Each item in the spinner also has custom layout .
I managed to show the data in the spinner. Can somebody help me in getting the selected value ?
I have tried spinner.getItemIdAtPosition(position) but i'm getting the result as 0 even if i click any item.
This is my adapter code:
class CustomDropDownAdapter(val context: Context, var batchList: Array<BatchList>) : BaseAdapter() {
val mInflater: LayoutInflater = LayoutInflater.from(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View
val vh: ItemRowHolder
if (convertView == null) {
view = mInflater.inflate(R.layout.batch_row_spinner, parent, false)
vh = ItemRowHolder(view)
view?.tag = vh
} else {
view = convertView
vh = view.tag as ItemRowHolder
}
vh.date.text = batchList.get(position).expiry_date
vh.availQty.text = "Available: ${batchList.get(position).available_quantity}"
return view
}
override fun getItem(position: Int): Any? {
return null
}
override fun getItemId(position: Int): Long {
return 0
}
override fun getCount(): Int {
return batchList.size
}
private class ItemRowHolder(row: View?) {
val date: TextView
val availQty: TextView
init {
this.date = row?.findViewById(R.id.date) as TextView
this.availQty = row?.findViewById(R.id.available) as TextView
}
}
}
This is my Function where i load in spinner
/**Method to load all items in spinner */
private fun loadBatch(medicineId:String,pharmaId:String)
{
val call=RetrofitClient.instance.api.displayBatchList("Bearer $token",20.toString(),medicineId,0.toString(),pharmaId)
call.enqueue(object :Callback<Array<BatchList>>{
override fun onResponse(call: Call<Array<BatchList>>, response: Response<Array<BatchList>>) {
if(response.code()==200)
{
var spinnerAdapter: CustomDropDownAdapter = CustomDropDownAdapter(context!!, response.body()!!)
updateMedView.pharmaSpinerbatch.adapter = spinnerAdapter
}
}
override fun onFailure(call: Call<Array<BatchList>>, t: Throwable) {
Log.e("Batch Load error",t.message)
}
})
}
I need to get the selected item
Add listner for your spiner
//item selected listener for spinner
mySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(p0: AdapterView<*>?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
Toast.makeText(this#MainActivity, myStrings[p2], LENGTH_LONG).show()
}
}

Custom ArrayAdapter with filter is not filtering correctly

I have the following ArrayAdapter:
class SearchAdapter(private val activity: Activity, private var species: ArrayList<Specie>) : ArrayAdapter<Specie>(activity, R.layout.specie_item, species) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
return convertView ?: createView(position, parent)
}
private fun createView(position: Int, parent: ViewGroup?): View {
val specie = species[position]
val view = LayoutInflater.from(context).inflate(R.layout.specie_item, parent, false)
view.specie_text.text = specie.name
return view
}
override fun getCount() = species.size
override fun getItem(position: Int) = species[position]
override fun getFilter() = filter
private var filter: Filter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
val results = FilterResults()
val query = if (constraint != null && constraint.isNotEmpty()) autocomplete(constraint.toString())
else arrayListOf()
results.values = query
results.count = query.size
return results
}
private fun autocomplete(input: String): ArrayList<Specie> {
val results = arrayListOf<Specie>()
for (specie in species) {
if (specie.name.toLowerCase().contains(input.toLowerCase())) results.add(specie)
}
return results
}
override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
if (results.count > 0) notifyDataSetChanged()
else notifyDataSetInvalidated()
}
override fun convertResultToString(result: Any) = (result as Specie).name
}
}
The purpose of this adapter is to show some suggestions when we type something in a AutoCompleteTextview. To do this, I have a filter that search for species names according to the user input. The problem is that this filter is not working as I expected:
Until here it's fine. It his showing all species names that start with agro
But after typing something more, it is not filtering anymore. It should show species names that start with agrostis cas, but it is still showing the Agrostis azorica.
Is my filter bad? I have tried some other ways to filter, but I got exactly the same result.
What you are missing currently is your adapter does not take into account anything about your filter, you set it to depend on the full species list with your get count, getItemPosition.
Also you should take care of updating your text when views are recycled by your adapter and not set the value only when views are created.
Something like that should be better :
class SearchAdapter(private val activity: Activity, private var species: ArrayList<Specie>) : ArrayAdapter<Specie>(activity, R.layout.specie_item, species) {
var filtered = ArrayList<Specie>()
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
return convertView ?: createView(position, parent)
}
private fun createView(position: Int, parent: ViewGroup?): View {
val view = LayoutInflater.from(context).inflate(R.layout.specie_item, parent, false)
view?.name?.text = filtered[position].name
return view
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View {
convertView ?: LayoutInflater.from(context).inflate(R.layout.specie_item, parent, false)
convertView?.name?.text = filtered[position].name
return super.getDropDownView(position, convertView, parent)
}
override fun getCount() = filtered.size
override fun getItem(position: Int) = filtered[position]
override fun getFilter() = filter
private var filter: Filter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
val results = FilterResults()
val query = if (constraint != null && constraint.isNotEmpty()) autocomplete(constraint.toString())
else arrayListOf()
results.values = query
results.count = query.size
return results
}
private fun autocomplete(input: String): ArrayList<Specie> {
val results = arrayListOf<Specie>()
for (specie in species) {
if (specie.name.toLowerCase().contains(input.toLowerCase())) results.add(specie)
}
return results
}
override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults) {
filtered = results.values as ArrayList<Specie>
notifyDataSetInvalidated()
}
override fun convertResultToString(result: Any) = (result as Specie).name
}
}

Categories

Resources