[Android] Expandable List View 사용하기

2022. 1. 21. 22:08개발을 파헤치다/Android

반응형

ExpandableListView

Expandable List Adapter구현

class CategoryListAdapter(
    private val context: Context,
    private val viewModel: RecyclerViewModel
): BaseExpandableListAdapter() {

    private var parentList = emptyList<Category>()
    private var childList = mutableListOf<List<Category>>()

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

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

    override fun getGroup(groupPosition: Int): Any {
        return parentList[groupPosition]
    }

    override fun getChild(groupPosition: Int, childPosition: Int): Any {
        return childList[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 false
    }

    /*
    *   부모 리스트 아이템 레이아웃 설정
    * */
    override fun getGroupView(
        groupPosition: Int,
        isExpanded: Boolean,
        convertView: View?,
        parent: ViewGroup?
    ): View {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val parentView = inflater.inflate(R.layout.category_parent_list_item, parent,false)
        val parentCategory = parentView.findViewById<TextView>(R.id.category_parent_name)
        val category = parentList[groupPosition]
        // 카테고리 이름 설정
        parentCategory.text = category.name
        // 기본 자식 카테고리를 설정한다
        val defaultCategory = Category(
            id = 0,
            slug = "",
            name = AppUtils.getString(R.string.defaultChildCategoryName),
            count = 0,
            hasChild = false
        )
        val childCategoryList = mutableListOf(defaultCategory)
        
        // 자식 카테고리가 존재할 경우 API 호출
        if(category.hasChild){

            val childCategoryViewModel = RecyclerViewModel()
            // API를 호출한다
            childCategoryViewModel.requestCategoryList(parent = category.id)
            // 데이터를 받았을 때 로직 구현
            childCategoryViewModel.dataList.observe(context as AppCompatActivity, Observer {
                if(it.isNotEmpty()){
                    val dataList = it.filterIsInstance<Category>()
                    // 기본 데이터가 들어있는 리스트와 API를 통해 받은 리스트를 합친다
                    childCategoryList += dataList
                    // 알맞은 자식 아이템의 데이터 셋을 변경해준다
                    childList[groupPosition] = childCategoryList
                }
            })
        }else{
            childList[groupPosition] = childCategoryList
        }


        return parentView

    }

    /*
    * 자식 리스트 아이템 레이아웃 설정
    * */
    override fun getChildView(
        groupPosition: Int,
        childPosition: Int,
        isLastChild: Boolean,
        convertView: View?,
        parent: ViewGroup?
    ): View {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val childView = inflater.inflate(R.layout.category_child_list_item, parent,false)
        val childCategory = childView.findViewById<TextView>(R.id.category_child_name)
        val item = getChild(groupPosition, childPosition) as Category
        childCategory.text = item.name
        return childView
    }

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

        return true
    }

    // 부모 데이터셋을 설정하는 메서드
    fun setGroupDataList(dataList: List<Category>){
        parentList = dataList
        notifyDataSetChanged()
    }

    // 자식 데이터셋을 설정하는 메서드
    fun setChildDataList(dataList: MutableList<List<Category>>){
        childList = dataList
        notifyDataSetChanged()
    }
}

자식 아이템을 API를 통해 받아와야 하는 경우 부모 아이템의 index 값을 알고 있어야 합니다. 그래야 부모 데이터 index에 맞게 자식 아이템을 매치할 수 있습니다. 이를 위해 부모 아이템 레이아웃을 생성할 때 API 호출 로직을 넣어주면 됩니다. 그럼 API 호출 후에 데이터를 받아와서 부모 아이템 index에 맞게 자식 아이템을 설정할 수 있습니다.

Activity에서 사용

  // 네비게이션 카테고리 리스트를 초기화하는 메서드
    private fun initCategoryList(){
        val categoryList = binding.navCategoryListView.findViewById<ExpandableListView>(R.id.nav_category_list_view)
        val categoryListAdapter = CategoryListAdapter(this, categoryViewModel)
        categoryList.setAdapter(categoryListAdapter)
        categoryList.setOnGroupClickListener { parent, v, groupPosition, id ->
            false
        }
        categoryList.setOnChildClickListener { parent, v, groupPosition, childPosition, id ->
            // 클릭시 부모 카테고리 값과 자식 카테고리 값을 포함시켜 카테고리 결과 액티비티로 이동한다
            val parentItem = categoryListAdapter.getGroup(groupPosition) as Category
            val parentCategoryName = parentItem.name
            val childItem = categoryListAdapter.getChild(groupPosition, childPosition) as Category
            val childCategoryName = childItem.name
            var categoryId = parentItem.id
            // 기본 하위 카테고리 이외에 다른 하위 카테고리를 선택한 경우 해당 카테고리 id값을 넘겨준다
            if(childItem.id != 0 && categoryListAdapter.getChildrenCount(groupPosition) > 1){
                categoryId = childItem.id
            }
            // 카테고리 검색결과 액티비티로 이동한다
            val intent = Intent(this, CategoryResultActivity::class.java)
            intent.putExtra(AppUtils.getString(R.string.parentCategoryNameKey), parentCategoryName)
            intent.putExtra(AppUtils.getString(R.string.childCategoryNameKey), childCategoryName)
            intent.putExtra(AppUtils.getString(R.string.targetCategoryIdKey), categoryId)
            startActivity(intent)
            false
        }

        // 부모 카테고리 리스트를 불러온 뒤 Live Data를 observing
        categoryViewModel.requestCategoryList()
        categoryViewModel.dataList.observe(this, Observer { it ->
            val parentCategoryList = it.filterIsInstance<Category>()
            // 부모 카테고리 데이터 설정
            categoryListAdapter.setGroupDataList(parentCategoryList)
            // 부모 카테고리 수만큼 자식카테고리 아이템을 생성해서 설정해준다
            // index out of bounds 에러를 막기 위함
            val childList = mutableListOf<List<Category>>()
            it.forEach {
                childList.add(emptyList())
            }
            categoryListAdapter.setChildDataList(childList)
        })
    }

부모 아이템에 속하는 자식 아이템을 API로 받아와야하는 경우에는 동기화 문제가 발생할 수 있습니다.
아무리도 2개의 List에서 index 값을 통해 부모 아이템과 자식 아이템을 매핑하다 보니 이게 조금만 틀어지면 바로 문제가 생깁니다. 부모 아이템과 자식 아이템이 매치가 안되거나, Index Out of Bounds 에러가 발생하는 것이죠.
이 문제를 해결하기 위해 empty list를 생성해서 미리 세팅해두었습니다. API 호출을 하게 되면 Main이 아닌 다른 Thread에서 Network 요청 처리가 된 다음 콜백 함수를 통해 자식 데이터가 세팅이 됩니다. 미리 empty list를 설정해 놓으면 나중에 데이터를 받더라도 index로 인해 문제가 생기지 않습니다. 

반응형