Search code examples
androidkotlinadapterexpandablelistview

ExpandableListAdapter shows wrong childViews


I'm writing an app to show a tree view of drug groups, drugs, and their information. Essentially, it's an ExpandableListView of drug groups, which shows individual drug names as the children views and opens a new fragment with more information on click.

I'm stuck with populating the child views with correct data. The adapter seems to get the group data correctly and from logging and debugging it seems that the child data is also passed on correctly. However, the text in the childViews in the ExpandableListView is only correct for the first group I open, every next group shows seemingly random contents (order of opening doesn't matter). The number of childViews is correct. The detail views (onClick) show correct info and on pressing the back button, the menu is then being showed with the correct info (however, any newly opened group then still shows wrong contents).

I've done at least 20 rounds checking and clarifying any dubious code but to no avail.

Screenshots for clarification:

list view with two groups expanded

detail view, showing correct info (but not matching that shown in list view)

list view upon returning (notice contents now shown correctly)

Here's the ExpandableListAdapter:

class MedicationsListAdapter(
    private val context: Context,
    private val groupList: List<String>,
    private val itemList: List<List<String>>
) : BaseExpandableListAdapter() {

    override fun getGroupCount(): Int {
        return groupList.size
    }

    override fun getChildrenCount(groupPosition: Int): Int {
        return itemList[groupPosition].size
    }

    override fun getGroup(groupPosition: Int): List<String> {
        return itemList[groupPosition]
    }

    override fun getChild(groupPosition: Int, childPosition: Int): String {
        return itemList[groupPosition][childPosition]
    }

    override fun getGroupId(groupPosition: Int): Long {
        return groupPosition.toLong()
    }

    override fun getChildId(groupPosition: Int, childPosition: Int): Long {
        return childPosition.toLong()
    }

    override fun hasStableIds(): Boolean {
        return true
    }

    override fun getGroupView(
        groupPosition: Int,
        isExpanded: Boolean,
        convertView: View?,
        parent: ViewGroup?,
    ): View {
        var groupView = convertView
        if (groupView == null) {
            val layoutInflater =
                this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            groupView = layoutInflater.inflate(R.layout.medication_list_group, null)

            val groupTextView: TextView = groupView.findViewById(R.id.text_group_name)
            groupTextView.text = groupList[groupPosition]
        } else return groupView
        return groupView
    }

    override fun getChildView(
        groupPosition: Int,
        childPosition: Int,
        isLastChild: Boolean,
        convertView: View?,
        parent: ViewGroup?
    ): View {
        var childView = convertView
        if (childView == null) {
            val layoutInflater =
                this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            childView = layoutInflater.inflate(R.layout.medication_list_item, null)

            childView.findViewById<TextView>(R.id.text_medication_name).text = getChild(groupPosition, childPosition)

        } else return childView
        return childView
    }

    override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean {
        return true
    }
}

Here's the detail view fragment:

class MedicationItemFragment : Fragment() {

    private lateinit var medicationName: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //get medication name from SafeArgs
        arguments?.let {
            medicationName = it.getString("medicationName").toString()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_medication_item, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // get the correct medication data
        val medication: Medication = MedicationsListData().getMedication(medicationName)

        // populate the view with current medication's data
        view.findViewById<TextView>(R.id.text_pharmacodynamics_body).text =
            medication.pharmacodynamics
        view.findViewById<TextView>(R.id.text_contraindications_body).text =
            medication.contraindications
    }

    companion object {
        fun newInstance(): ParametersFragment = ParametersFragment()
    }
}

Here's the class providing the adapter's data:

class GroupListData {
    fun getItemData(): List<List<String>> {
        return listOf(
            listOf("amoxicillin + clavulanate","penicillin","clindamycin","vancomycin"),
            listOf("epinephrine","norepinephrine","dopamine"),
            listOf("metoprolol","adenosine","amiodarone"),
            listOf("metoclopramide")
        )
    }

    fun getGroupData(): List<String> {
        return listOf(
            "antibiotics",
            "vasopressors",
            "antiarrhythmics",
            "antiemetics"
        )
    }
}

I can elaborate or explain anything if neccessary. Any help is very much appreciated!


Solution

  • After reading around I now understand that the problem is the recycling views not populating correctly. A fix to this was to just stop recycling the views and creating a new one for every child instead:

        override fun getChildView(
            groupPosition: Int,
            childPosition: Int,
            isLastChild: Boolean,
            convertView: View?,
            parent: ViewGroup?
        ): View {
            var childView = convertView
            val layoutInflater = this.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            childView = layoutInflater.inflate(R.layout.medication_list_item, null)
    
            val medicationName = GroupListData().getItemData()[groupPosition][childPosition]
            val medication: Medication = MedicationsListData().getMedication(medicationName)
    
            childView.findViewById<TextView>(R.id.text_medication_name).text = medication.medicationName
            if(medication.brandName != "null") {
                childView.findViewById<TextView>(R.id.text_brand_name).text = medication.brandName
            }
    
            return childView
        }
    

    I wouldn't exactly consider this a solution as much as a workaround, since the amount of (non-recycled) views will probably cause some performance issues. But if anyone else is struggling with the same issue (and doesn't anticipate performance problems), this works.