마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

ConcatAdapter로는 순서를 섞어서 사용 못하나요?

0 추천
A_adapter = AAdapter(A_Items)
B_adapter = BAdapter(B_Items)
concatAdapter = ConcatAdapter(A_adapter, B_adapter)

ConcatAdapter를 사용할때, concatadapter에 set할때 이러한 형식으로 Adapter들을 셋한다고 가정하겟습니다.

그럼 아이템은 A 어댑터 이후 B 어댑터를 넣었으니 A아이템 나열이후 B아이템이 나열 될겁니다.

AAAABBBBB  이렇게요.

그런데 이걸 좀 유동적으로 순서를 섞을 순 없나요?

예를들면 ABBAAAABBBABABABABBB 이런식으로요..

즉 A아이템이 나오고 B 아이템이 나열이 되고 다시 A아이템이 나열되는 이러한 방식들이요..

이런걸 사용하려면 그냥 하나의 어댑터에 뷰타입을 검사하는 방법 밖에 없는지요

 

 

codeslave (3,940 포인트) 님이 2021년 12월 3일 질문
codeslave님이 2021년 12월 5일 수정
됩니다. 대신 어댑터 리스트를 다시 만들어서 다시 concatAdapter에 설정해 주시거나
add, remove를 하셔서 변경하시면 됩니다.
제가 보기에는 님의 경우에는 RecyclerView하나와 Adapter하나만 있으면 되고 멀티뷰를 사용하시면 될 듯한데요.
디테일에 해당하는 리스트만 가져와서 ItemDecorator 나 간단하게 리스트의 조작을 통해 처리하면 될 것 같은데, 복잡하게 처리하시는 것 같아요.

아래 링크의 답글을 보시면, Task들만 가져와서 그룹을 만들어서 RecyclerView에 보여주는 예제코드가 있습니다.
https://www.masterqna.com/android/99317/%EB%A9%B0%EC%B9%A0-%EC%A7%B8-%EC%A4%91%EC%B2%A9%EB%A6%AC%EC%82%AC%EC%9D%B4%ED%81%B4%EB%9F%AC%EB%B7%B0-mvvm%EC%9C%BC%EB%A1%9C-%EA%B3%A8%EB%A8%B8%EB%A6%AC%EB%A5%BC-%EC%95%93%EA%B3%A0-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4-%E3%85%A0%E3%85%A0

제가 볼 때는 그거랑 님이 만드시고 계신화면이랑 같은 구조거든요. 다시 한번 화면에 보여주어야할 것과 DB에서 가져와야 할 데이터를 잘 분석해 보세요.
넹 저도 이제 RecyclerView 하나만 사용하고 있어요.. Epoxy사용했는데 지금 EditText에서 값을 저장하고 불러오는거에서 지금 막혀서..원인을 모른채 해결을 못해서 그냥 기존의 리사이클러뷰만 사용하려고 고민중인데요...
조금이라도 덜 복잡하게 하려고 어떤걸할지 고민중하던중
ConcatAdapter를 사용할까 했는데 제가 본문에 적어놓은게 되는지 안되는지 정확하지 않아서..구글링해도 이거관련해서는 잘안나오더라구요.
Git을 뒤져보니 똑같지는 않지만 Expandable ConcatAdapter를 사용하던데,
어댑터 리스트를 만들어서 아이템하나에 이 Adapter하나를 적용 시키는거같더라구요..
혹시 선생님이 말씀하시는게 이건지요?(어댑터 리스트를 만들어서 아이템 하나에 리스트 하나를 저장하는 형태 - https://github.com/OHoussein/Android-Expandable-ConcatAdapter 제가본 Git 코드..)
아니면 선생님은 그냥 ConcatAdapter보다는 선생님이 걸어놓은 링크내용처럼
일반 정통의 RecyclerView와 Adapter만 사용하고, ViewType을 검사하는 형태 리스트를 나열하는 방법으로 하는걸 더 추천하시는건가요?
ABBAAAABBBABABABABBB의 예를 드셨을 때, A,B를 아이템타입이 아니라 어댑터라고
생각했었어요. Expandable List나 Grouped List면 괜찮을 것 같네요. ConcatAdapter가 어떤 기능을 제공하는지 보려면 API문서나 개발자 문서가 더 정확해요.
https://developer.android.com/reference/androidx/recyclerview/widget/ConcatAdapter

해당 개발자 블로그도 정확한 정보를 얻기에 좋습니다.
https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a

addAdapter, removeAdapter를 사용하시면 될 것 같구요. 접히는 어댑터의 경우는 emptyList를 사용하거나 removeAdapter를 해볼 수 있을 것 같네요.
그런데, 동적으로 add, remove 하려면 로직이 좀 필요할 것 같네요. 왜냐하면 뷰모델에서 아이템을 어댑터를 생성할 구조에 맞게 리턴하고, 이걸 기반으로 어댑터를 구성해야 하기 때문에, 추가적인 코드가 더 들어갈 듯 합니다. 코드는 길어지지만, 관리에는 더 깔끔한 구조가 될 것 같아요.
님의 구조에 ConcatAdapter를 사용하려면 List<Pair<Header, List<ChildItem>>>과 같은 구조의 데이터를 뷰모델에서 리턴해주어야 합니다. 그리고 여기에 기반하여 뷰쪽에서 어댑터를 구성해 주면 됩니다.

val adapterItems: List<Pair<Header, List<ChildItem>>> = ....
for ((header, list) in adapterItems) {
     concatAdpater.addAdapter(HeaderAdapter(header))
     concatAdpater.addAdapter(ChildItemAdater(list))
}

또는 adapterList = arrayListOf<RecyclerView.Adapter>()
for ((header, list) in adapterItems) {
     adapterList.add(HeaderAdapter(header))
     adapterList.add(ChildItemAdater(list))
}

concatAdapter.adapter = adapterList
선생님 감사합니다. Expandable ConcatAdapter를 사용하면 제가 의도하던 UI는 좀 무너질것같지만 처음 댓글에 링크한 Git에 코드도 나와있고 조금 편할 수는 있겠네요..
그런데 제가 무작정 ConcatAdapter만 보고 사용하겠다고 했을때는 생각이 안들었는데 선생님이 작성해주신 코드나 위의 Git 코드를 보고 조금 생각하다가 궁금한점이 있어서 질문좀 드립니다.

ConcatAdapter를 사용해서 아이템의 순서를 유동적으로 섞이게 가져가려면, 결국 하나의 어댑터로는 불가능하고 말씀대로 아이템 하나당 하나의 어댑터를 할당(본문 사진 참고...)해서 추가/삭제하는 방식을 사용해야할것같은데요,
여기 Expandable ConcatAdapter (https://github.com/OHoussein/Android-Expandable-ConcatAdapter) 코드에서도 그렇게 진행(Adapter를 리스트에 저장하고 Expnadable 아이템하나당 Adapter 한개)하고, 보니까 선생님이 올려주신 코드도 Adapter를 아이템 하나당 할당?해서 하시는것 같은데요.

여기서 궁금한것이..해당 깃 링크에서도 그렇고 선생님 코드에서도 그렇고
아이템 하나당 Adapter를 설정 해버리면요 어댑터하나당 아이템이 충분하지 않으므로 재활용이 되지 않을것 같은데 이게 리사이클러뷰로서의 의미를 가질 수가 있나요?

제가 올린 깃링크의 사람은 사용하는 어댑터도 뷰타입을 구분해서 하나의 Expandable 아이템 리스트에 Header와 Detail까지 같이 있어 아이템이 조금 있기라도한데,
선생님의 경우에는 Header 어댑터와 ChildAdapter를 따로 구분해놓으셨잖아요?Header 어댑터만 따로있다면, 헤더아이템은 아이템당 Header 어댑터가 설정이 될텐데 그럼 이 어댑터에는 Header 아이템이 한개만 존재하는것 아닌가요?

이렇게되면 리사이클러뷰로서의 의미가 있는지 궁금합니다..

아직 ConcatAdapter를 사용할지 어댑터하나에 뷰타입 구분으로 할지 못정했지만...어떤게 더 나을지도 궁금하네요..
제가 알기로는 ConcatAdapter. 내부적으로는 Adapter 간에 같은 뷰를 재사용하도록  되어 있는 걸로 압니다. 그리고 아이템 숫자가 충분하지 않은 경우는 Adapter를 하나만 사용할 때도 마찬가지입니다.
그리고 제가 올린코드는 아이템당 어댑터가 하나가 아니라 그룹당 어댑터가 두개 (헤더 + 자식리스트) 예요. 그리고 헤더가 존재하지 않고 자식아이템만 필요할 경우는 헤더에 아이템이 없는 경우는 자식 아이템만 추가하는 로직을 집어 넣으면 되구요.

val adapterList = arrayList<RecyclerView.Adapter()
for ((header, list) in adapterItems) {
     if (header.haItem()) {
         adapterList.add(HeaderAdapter(header))
     }
     if (list.isNotEmpty() {
          adapterList.add(ChildItemAdater(list))
     }
}

ConcatAdapter는 기본적으로 멀티뷰와 동일한데 한개의 어댑터 내에서 처리하지 않고 별도로 분리해서 코드를 더 깔끔하게 가져간다고 보시면 될 것 같아요.
아 그렇죠..실수했네요. 그룹당 두개..그러니까 헤더아이템 1개 자식아이템들 묶어서 어댑터 1개..
그렇다면 선생님 Header의 경우 그룹당 헤더가 1개이므로 HeaderAdapter에서 아이템 사이즈가 1로 고정이 될텐데 이것도 괜찮은것이죠?

그리고
(https://github.com/OHoussein/Android-Expandable-ConcatAdapter)
(https://github.com/tarik-git/ExpandableYT/tree/master/app/src/main/java/com/example/expandableyt)
이 두곳 보시면 둘다 어댑터 한개로만 하고 뷰타입을 구분해서 사용하는 방식을 쓰던데, 이렇게 하면 굳이 ConcatAdapter를 사용하는 이점을 별로 못가져 오죠?

ConcatAdapter를 사용할때 선생님이 하신것처럼 Header와 자식 Adapter 두가지가 있다면, DiffUtil이라던지 뭐.. 아이템 관리라던지.. 각각 영역?이 분리 되어있으니 더 구분하기도 쉬울것같구요..장단점 같은거야 있겠지만 ConcatAdapter를 사용하는 편이 여러모로 편리하겠죠?
헤더아이템이 한개이던 여러개든 상관없습니다. 어차피 어댑터이고 리스트를 받게 되어있으니까, 필요한 상황에 따라 데이터를 만들어 주면 되는 거니까요? 말씀드렸으니, 어댑터를 하나만 사용하는 거랑 큰 차이가 없어요. 그리고 자료구조도 님이 취향이나 상황에 따라 저처럼 해도 되고, 다른 Github예제처럼 해도 됩니다. 빠르고 쉬운 걸 선택하면 됩니다.
감사합니다. 선생님이 다신 링크(다른분이 질문한것..)를 보았는데, 저랑 비슷한 것을 구현하시는것같던데 선생님이 답변하신걸보니까 Adapter한개에 멀티뷰타입을 해서 구분해서 하시는것 같더라구요, 그리고 TaskListItem에 레이아웃 아이디를 넣고 구분하구요.

여기서 정말 단순한 질문이 있는데요, 저는 ConcatAdapter로 한번 구현을 시도해볼까하는데요.. 그렇다면 저의경우에는 링크의 TaskItem처럼 굳이 sealed 클래스로 Header와 Child 아이템까지 둘 필요는 없죠? 그냥 각각의 data class만 존재해도 될까요?

그리고 이 둘을 Pair로 묶어주기만 해도되나요
어댑터를 하나만 쓰던 ConcatAdapter를 사용하던 구현이 좋고 편한 쪽으로 하시면 될 것 같아요. 당연히 어떤 어탭터를 사용하느냐에 따라 자료구조도 달라지겠죠.
그리고 저같은 경우는 어댑터 하나에 멀티뷰를 구현했기 때문에  sealed class를 사용한 겁니다. sealed class 의 장점 중의 하나가 when을 사용할 때 컴파일러가 해당 sealed class의 child class를 전부 체크하고 있는지 검사해 준다는 겁니다.
만약
when (item) {
    is ListItem.Header -> doSomething()
}

위처럼만 했다면 Kotlin compiler가  해당 sealed class 에는 ListItem.Item도 존재하는데 when 문장에서 사용하지 않기 때문에 경고를 날려줍니다. 버그를 방지하기에 더 좋죠.
당연히 그냥 data class 를 사용해도 됩니다. 정답은 없고 님의 선택에 달린 문제입니다.
그럼 제가 그룹당 한개의 헤더를 가져갈 예정인데요
val adapterItems: List<Pair<Header, List<ChildItem>>> = ....
for ((header, list) in adapterItems) { // 반복한번당 그룹한개.(헤더1 상세 x개)
     concatAdpater.addAdapter(HeaderAdapter(header))
     concatAdpater.addAdapter(ChildItemAdater(list))
}

또는 adapterList = arrayListOf<RecyclerView.Adapter>()
for ((header, list) in adapterItems) {
     adapterList.add(HeaderAdapter(header))
     adapterList.add(ChildItemAdater(list))
}

concatAdapter.adapter = adapterList

이 코드를 예시로하면 그룹당 Header는 하나만 존재하기때문에 또한 HeaderAdapter에 헤더아이템은 단 하나만 들어가는데,
이건 별 상관 없다는 말씀이시져?

프로젝트 하나만들어서 테스트코드를 진행해봣는데 제가 쓰고도 올바른건지 몰겠네요ㅋㅋ아이템이 한개만 존재하는 어댑터는 처음이라 이게 어댑터로써의 가치가 있는지 의문이었어서 자꾸 여쭈어보게되네요 죄송합니다ㅠ
네. 어차피 어댑터의 아이템 갯수가 한개이냐 아니냐는 중요하지 않아 보여요. ConcatAdapter 내부적으로 optimisation 을 하기 때문에 퍼포먼스도 별로 걱정하지 않아도 되구요.
저도 ConcatAdpater를 사용 중인데 아이테이 하나만 존재하는 어댑터들이 있어요. 이건 이슈가 아니라고 봐요. 어댑터가 어차피 0 - n개의 아이템을 받아드리도록 설계가 된거니까.

2개의 답변

0 추천

참고로 뷰모델에서 ConcatAdapter용으로 데이터를 변환하는 코드입니다. 님이 사용하는 데이터 구조에 따라 다르게 처리가 되긴 해야되겠죠.

val itemsByDao: List<TaskEntity> = dao.getAllTasks()

// List<Pair<String, List<TaskListItem.Item>>> 타입을 리턴할 경우
itemsByDao.groupBy { it.start_time }
     .entries
     .map { (header, list) ->
                header to list.map { task -> TaskListItem.Item(task) 
      }

// List<Pair<String, List<TaskEntity> 타입을 리턴할 경우
itemsByDao.groupBy { it.start_time }.toList()
   
     

이렇게 할 때, 한가지 확인해 보실 점은 변환한 결과의 순서가 정렬순서가 제대로 되었는지 여부입니다. 

 

spark (227,530 포인트) 님이 2021년 12월 4일 답변
0 추천

참고로 Adapter를 만들 때 기본 뼈대를 제공해주면 보일러 플레이드 코드없이 손쉽게 Adapter를 처리할 수 있을 것 같습니다.

abstract class GenericAdapter<T: ListItem>(initialItem: List<T> = emptyList(), diffCallback: DiffUtil.ItemCallback<T> = BasicItemCallback<T>()) :
    ListAdapter<T, GenericViewHolder<T>>(diffCallback) {

    init {
        this.submitList(initialItem)
    }

    override fun getItemViewType(position: Int): Int = getItem(position).layoutId

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenericViewHolder<T> {
        val itemView = LayoutInflater.from(parent.context)
            .inflate(viewType, parent, false)
        return createViewHolder(itemView)
    }

    abstract fun createViewHolder(itemView: View): GenericViewHolder<T>

    override fun onBindViewHolder(holder: GenericViewHolder<T>, position: Int) {
        holder.bind(getItem(position))
    }
}

interface ListItem {
    val layoutId: Int
}

이렇게 베이스 클래스를 만들고

class BasicItemCallback<T> : DiffUtil.ItemCallback<T>() {
    override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
        return oldItem == newItem
    }

    override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
        return oldItem == newItem
    }
}

class TaskHeaderAdapter(item: TaskListItem.Header) :
    GenericAdapter<TaskListItem.Header>(initialItem = listOf(item)) {

    override fun createViewHolder(itemView: View): GenericViewHolder<TaskListItem.Header> {
        return TaskHeaderViewHolder(itemView)
    }
}

class TaskHeaderViewHolder(
    itemView: View
) : GenericViewHolder<TaskListItem.Header>(itemView) {

    private val binding by lazy { ItemTaskHeaderBinding.bind(itemView) }

    override fun bind(item: TaskListItem.Header) {
        binding.apply {
            startDateTxt.text = item.startTime
        }
    }
}

class TaskItemAdapter(tasks: List<TaskListItem.Item> = emptyList()) :
    GenericAdapter<TaskListItem.Item>(initialItem = tasks) {

    override fun createViewHolder(itemView: View): GenericViewHolder<TaskListItem.Item> {
        return TaskItemViewHolder(itemView)
    }
}

class TaskItemViewHolder(
    itemView: View
) : GenericViewHolder<TaskListItem.Item>(itemView) {

    private val binding by lazy { ItemTaskBinding.bind(itemView) }

    override fun bind(item: TaskListItem.Item) {
        val task = item.task
        binding.apply {
            val alarmResId = if (task.hasAlarm) R.drawable.ic_alarm_on else R.drawable.ic_alarm_off
            alarmImg.setImageResource(alarmResId)
            contentTxt.text = if (task.isDone) task.content.strikeThrough() else task.content
            endDateTxt.text =
                if (task.end_time.isNotBlank()) "~ ".plus(task.end_time.takeLast(5)) else ""
        }
    }
}

위와 같은 식으로 처리하면 코드량이 많이 줄어들 것 같습니다. 좀더 리팩토링하면 createViewHolder같은 부분도  더 간결하게 처리할 수 있을 듯합니다. 아무튼 보완하면 위의 코드보다 더 좋은 코드를 만들 수 있을 겁니다.

spark (227,530 포인트) 님이 2021년 12월 4일 답변
...