리 사이클러 뷰 사용 시
리사이클러 뷰 사용시, 위처럼 아이템들이 재활용되는것을 알았지만, 메서드들을 어디에서 호출해야 하는지 정확히 이해하지 못하고 아무곳이나 쓰곤 했다. 하지만, 리사이클러 뷰에서 사용시에 알아두면 좋을 지식을 검색하게 되었고, 리사이클러 뷰를 활용할 때 기억해 두면 좋을 3가지 안티 패턴을 소개하고자 한다.
참고 블로그
안티패턴 - 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에서 직접 역할을 분리해 줄 수 있다면, 코드를 훨씬 유지 보수하기 쉬워질 것이다.
스와이프에 관련된 글은 다음 블로그 글을 참조하면 좋다.
최근댓글