android

[안드로이드] RecyclerView 공통 어댑터 개편기

sieunju 2022. 12. 25. 23:33
반응형

안녕하세요. 이번 포스팅에서는 제가 서비스중인 곳에 공통 어댑터를 구축하면서 발생한 이슈들 그리고 개편한 내용에 대해 포스팅 해보겠습니다.


각 타입별 어댑터들의 장, 단점은 아래와 같이 정리해볼수 있겠습니다.

타입별 어댑터 장, 단점 비교

여러 개발자들과 동시에 다른 개발을 하다보면 공통 어댑터는 오히려 마이너스 요소라 화면 단위로 어댑터를 생성하는것이 좋아보입니다. 하지만, 단순 반복적인 행위를 하다보면 클래스들은 점차 많아지고 이 또한 리소스 낭비라고 생각이 듭니다.

그래서 어쩌라는거지..?

저는 개발자의 단순 반복 작업은 최대한 줄이고, 개발에만 집중하는 방식을 추구합니다. 따라서, 공통 어댑터를 사용하고 대신 어떻게 해서든 소스 충돌이 발생하지 않는 어댑터로 만들기로 했습니다.

소스 충돌이 발생하는 곳은 아래와 같습니다.

  • 공통 DiffUtil Class에서 아이템 비교를 처리하는 함수
  • 공통 어댑터에서 “onCreateViewHolder” 호출할때 viewType에 맞게 ViewHolder를 리턴하는 함수

공통 DiffUtil의 소스 충돌로 부터 해방!

아이템 추가 / 삭제에 대해서 불필요한 notifyDatasetChanged, notifyItemRangeChanged와 같은 함수를 개발자가 직접 처리하지 않기 위해 DiffUtil 을 적극적으로 사용하고 있습니다. 따라서 DiffUtil을 공통으로 만들어 사용합니다.

DiffUtil의 areItemsTheSame 와 areContentsTheSame 함수에 모든 아이템들의 비교 로직을 아래와 같이 “추가" 해야 하고, 동시에 여러 화면을 개발하다보면 소스 충돌이 100% 발생하게 됩니다.

소스 충돌 100% 발생할 소지가 충분히 있는 곳입니다.

해결

공통 어댑터를 사용할때 추가하는 클래스들이 있습니다. 그림으로 표현하면 아래와 같습니다.

1:1:1 관계도 입니다.

위 환경을 잘 이용해서 BaseUiModel에서 DiffUtil에서 사용하는 아이템 비교 로직을 처리 한다면 더이상 공통 DiffUtil 클래스에 “추가" 하는일은 없어지게 됩니다.

개선된 방식의 공통 DiffUtil Class

더이상 공통 DiffUtil 커밋도..소스 충돌도 없습니다.


공통 어댑터의 소스 충돌로 부터 해방!

공통 어댑터를 사용하다 보면 “onCreateViewHolder” 에서 ViewType 에 맞게 적절한 ViewHolder를 리턴해야 하기 때문에 “무언가를 추가" 해야 합니다.

이정도 코드 줄에서 한번더 있다는 사실….

강조되고 반복되는 작업은 개발자를 피곤하게 해요!!

해결

DiffUtil 에서 사용했던 방법과 비슷합니다. 이또한 역시 BaseUiModel 에서 “무언가" 를 선언하고 그걸 공통 어댑터에서 적절하게 처리를 하는 방식으로 해결 했습니다. 🤩

처리한 순서는 아래와 같습니다.

  • BaseUiModel에서 사용하는 ViewHolder Class 를 리턴하는 함수를 선언합니다.
  • 공통 어댑터에서 setData 를 할때 Map 에 ViewHolder Class Type을 저장합니다.
  • RecyclerView.Adapter -> onCreateViewHolder 함수 호출시 Map 에 데이터를 꺼내와서 적절한 ViewHolder 를 리턴합니다.

공통 어댑터.kt

fun submitList(newList: List<BaseUiModel>?) {
  ...
  setViewHolderMap(newList)
}
private fun setViewHolderMap(newList: List<BaseUiModel>) {
    // Map Key: ViewType, Value: KClass<out BaseViewHolderV2<*>>
    newList.forEach { model ->
        if (viewHolderTypeMap[model.layoutId] == null) {
            viewHolderTypeMap[model.layoutId] = model.getViewHolderType()
        }
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolderV2<*> {
    return try {
        getViewHolderType(parent, viewType)
    } catch (ex: Exception) {
        val notInvalidViewHolder = getViewHolderName(viewType)
        getLegacyViewHolder(parent, viewType)
    }
}

private fun getViewHolderType(parent: ViewGroup, viewType: Int): BaseViewHolderV2<*> {
    val viewHolderType = viewHolderTypeMap[viewType]
        ?: throw NullPointerException("ViewHolderMap Not Found")
    val findConstructor = viewHolderType.primaryConstructor
        ?: throw IllegalArgumentException("Not Found PrimaryConstructor")
    return if (findConstructor.parameters.size == 1) {
        // Parent Type
        findConstructor.call(parent)
    } else {
        // Parent, ViewModel Type
        findConstructor.call(parent, viewModel)
    }
}

 

공통 어댑터에서 사용하는 ViewHolder 를 생성시 매개변수의 경우의 수를 설명하자면 아래와 같습니다.

  • ViewModel 를 연결해서 사용하는 경우 ViewHolder(parent, viewModel)
  • ViewModel 를 연결해서 사용하지 않는 경우 ViewHolder(parent)

위 예시 코드에 대해서 자세히 설명하자면 데이터를 set 하는 부분에서 ViewType 과 ClassType을 맵에 put 하고 “onCreateViewHolder” 에서 ViewType에 맞는 ClassType을 리턴합니다.

 

비용 감소

공통 어댑터가 하나만 있는게 아니라 여러개가 존재합니다. 예를 들면 ViewPager2 에 사용하는 공통 PagerAdapter 등등..

즉, BaseUiModel 에서 추상화 함수를 추가한다면 모든 공통 어댑터에 선언된 ViewHolder 들을 연결해야합니다. 비용적인 측면으로 낭비가 되기 때문에 “getViewHolderType” 함수를 abstract 로 안하고 open 으로 처리하면서 특정 공통 어댑터에만 한번 사용하면서 상황을 지켜보고 추후 안정화가 되면 모든 UiModel 에 추가할 예정입니다.

 

TO-BE

개발자는 개발할 화면에 대한 ViewHolder 에서만 집중할수 있게 됩니다.

 

회고

  • 신기할정도로 여러 회사 기술 블로그에는 "공통 어댑터" 에 대한 고민글이 없었습니다. 못찾는거일수도 있지만, 다들 화면 단위로 어댑터를 고수하는지는 모르겠지만, 저는 그럼에도 불구하고 공통어댑터를 개편하고자 많은 노력과 고민을 하여 좋은 경험을 한거 같습니다. 
  • 공통 어댑터의 기본적인 어댑터의 장점은 불필요한 어댑터 생성을 안해도 된다는 큰 장점이 있습니다. 하지만, 필연적인 단점으로는 공통된 부분에 ‘무언가를 선언’ 해야 하다보니 소스 충돌은 무조건 발생하는 단점이 있습니다.
  • 공통 어댑터는 포기하기는 싫고..그래서 많은 고민과 연구를 하다보니 위와 같은 방안들이 생각나면서 소스 충돌이 아예 없는 공통 어댑터를 구현 했습니다. 즉, 두마리 토끼를 잡은셈이라 이후 개발 생산성이 많이 증대 될것으로 생각이 됩니다. 🤩

 

참고

 

해당 글은 제 개인 미디엄에서 따왔습니다. :) 긴글 읽어주셔서 고맙습니다.

 

https://github.com/sieunju

 

sieunju - Overview

with @isemang 🥭. sieunju has 23 repositories available. Follow their code on GitHub.

github.com

 

반응형