리 사이클러 뷰 사용 시

리사이클러 뷰 사용시, 위처럼 아이템들이 재활용되는것을 알았지만, 메서드들을 어디에서 호출해야 하는지 정확히 이해하지 못하고 아무곳이나 쓰곤 했다. 하지만, 리사이클러 뷰에서 사용시에 알아두면 좋을 지식을 검색하게 되었고, 리사이클러 뷰를 활용할 때 기억해 두면 좋을 3가지 안티 패턴을 소개하고자 한다.

 

참고 블로그

 

[번역] — RecyclerView 안티 패턴

본 글은 Aung Kyaw Paing님의 글을 한국어로 번역한 글입니다. 원문 링크 👇

medium.com

 

안티패턴 - 1. 어뎁터 내부 로직을 재활용하지 않는 것

 

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val itemAtPosition = itemList[position]
        holder.tvText.text = itemAtPosition.text
        holder.tvText.setOnClickListener {
            onItemClick(itemAtPosition)
        }
    }

위와 같은 코드를 작성하게 되면, 매번 새로운 리스너를 아이템 뷰에 설정해 주고 있기 때문에, 모든 아이템뷰에 리스너가 딸려있는 상태가 된다. 즉, 하나의 코드를 재활용하지 않는 코드가 된다는 것으로 해석할 수 있다.

 

그렇다면 어떻게 바꾸어야 할까?

private val onTextViewTextClicked = { position: Int ->
    onItemClick.invoke(itemList[position])
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
    return MyViewHolder(itemView, onTextViewTextClicked)
}

inner class MyViewHolder(
    itemView: View,
    private val onTextViewTextClicked: (position: Int) -> Unit
) : RecyclerView.ViewHolder(itemView) {
    val tvText: TextView = itemView.findViewById(R.id.textView)  
init {
        tvText.setOnClickListener {
            onTextViewTextClicked(adapterPosition)
        }
    }
}

위처럼 onTextViewTextClicked라는 람다 함수를 만들어 준 뒤에 이를 MyViewHolder 클래스 안에서 생성자로 받아 (함수 포인터처럼) 사용을 하는 것이다. 이렇게 된다면 아이템 뷰가 생성될 때마다, onTextViewTextClicked 함수가 계속 생성되는 일이 발생하지 않게 될 것이다.

 

안티 패턴 - 2. 어뎁터 내부에 많은 로직을 가지고 있는 것

 

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
    val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
    return MyViewHolder(
        itemView = itemView,
        onTextViewTextClicked = { position: Int ->
            val itemAtIndex = itemList[position]
            val intent = getDetailActivityIntent(itemAtIndex)
            parent.context.startActivity(intent)
        })
}

현재 위와 같은 코드를 보면, onCreateViewHolder에 많은 로직이 수행되고 있는 것을 볼 수 있다 ( 클릭 시 다른 activity로 넘어가게 하는 로직) 이와 같은 로직이 내부에 결합되어있으면, 이를 재활용하기 쉽지 않다. 그리고 또한, 코드가 복잡해져, 각 계층의 역할이 제대로 분리되지 않음을 알 수 있다. 

 

그래서,

class RecyclerViewAdapter(
    private val onAddClick: (itemAtIndex: Data) -> Unit,
    private val onRemoveClick: (itemAtIndex: Data) -> Unit,
    private val onItemClick: (itemAtIndex: Data) -> Unit
)

위처럼 어뎁터에 생성자로 실행 함수들을 인자로 받으면서, 뷰 모델과 같은 곳에서 로직을 가지고 있는 것이 훨씬 좋아 보인다.

 

안티 패턴 3. 부내에서 직접 뷰의 상태를 변경하는 것

 

프로젝트를 진행하면서 item swipe를 할 경우에 많은 코드들이 view의 tag값을 변경하여 swipe를 구현하는 것을 알 수 있다.

 

private fun getTag(viewHolder: RecyclerView.ViewHolder) : Boolean =  
    viewHolder.itemView.tag as? Boolean ?: false
private fun setTag(viewHolder: RecyclerView.ViewHolder, isClamped: Boolean) { 
    viewHolder.itemView.tag = isClamped 
}

이런 식으로 tag값을 관리하면 발생하는 문제점은, tag값을 가진 ItemView가 재활용되면서 아래로 스크롤했을 때에도 계속 열린 상태가 지속된다는 점이다.

 

이런식으로 아래 부분에도 닫기 버튼이 보인다.

이를 방지하기 위해서는, RecyclerViewAdapter에 들어가는 데이터 클래스에서 IsSwiped와 비슷한 변숫값을 지정해 주는 것이다.

data class Issue(
    val issueId: Int,
    val mileStone: String,
    val title: String,
    val contents: String,
    val label: Label,
    var isSwiped: Boolean = false
)

이렇게 되면 isSwiped 변수를 통해서 개별 ItemView가 열렸는지 닫혔는지 Adpater에서 판단하여 Swiped 되었는지를 판단해주기만 하면 되기 때문이다. 

 

또한, 위의 getTag 함수와 setTag 함수를 ViewModel에서 로직으로 관리해주면 더욱 역할 분리를 할 수 있다. 

data class Issue(
    val issueId: Int,
    val mileStone: String,
    val title: String,
    val contents: String,
    val label: Label,
    var isSwiped: Boolean = false
)



itemTouchHelperCallBackClass. kt

private fun getIsSwiped(viewHolder: RecyclerView.ViewHolder): Boolean =
    viewModel.getIssueSwiped(viewHolder.adapterPosition)

private fun setIsSwiped(viewHolder: RecyclerView.ViewHolder, isClamped: Boolean) {
    viewModel.changeIssueSwiped(viewHolder.adapterPosition,isClamped)
}



viewModel.kt

fun changeIssueSwiped(index: Int, isSwiped: Boolean) {
    _issueList.value[index].isSwiped = isSwiped
}

fun getIssueSwiped(index: Int): Boolean {
    return _issueList.value[index].isSwiped
}


fragment.kt

viewLifecycleOwner.repeatOnLifecycleExtension(Lifecycle.State.STARTED) {
    viewModel.issueList.collect {
		  submitList(it)
    }
}


adapter.kt

class IssueViewHolder(private val binding: ItemIssueRecyclerViewBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(issue: Issue) {
            binding.issue = issue
            if(!issue.isSwiped) { binding.cvSwipeView.translationX = 0f }
            else {
                binding.cvSwipeView.translationX = binding.root.width * -1f / 10 * 3
            }
...........

이렇게 ViewModel에서 직접 역할을 분리해 줄 수 있다면, 코드를 훨씬 유지 보수하기 쉬워질 것이다.

 

위의 코드들을 적용한 뒤

 

스와이프에 관련된 글은 다음 블로그 글을 참조하면 좋다. 

 

RecyclerView 에서 item Swipe 하기 (feat. ItemTouchHelper, ItemTouchUIUtil)

리사이클러뷰에서 아이템뷰 스와이프시 일정 길이만큼 스와이프가 된다스와이프 된 영역에 해당 아이템을 삭제할 수 있는 영역이 보이게 된다위와 같은 느낌으로 생각하면 된다아이템 뷰는 Fra

velog.io

 

 

728x90
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기