Beeeam

RecyclerView 마스터하기 본문

Android

RecyclerView 마스터하기

Beamjun 2023. 11. 24. 20:30

이번 프로젝트에서 피드 부분을 담당하게 되서 RecyclerView를 많이 다뤘다. 덕분에 이전보다 RecyclerView를 사용하는게 익숙해지긴 했지만 아직도 어렵게 느껴진다.

프로젝트의 안드로이드 파트 멘토 형이 했던 말이 생각난다.

RecyclerView는 금쪽이다.

 

이 말에 100% 공감한다. 얘는 잘 구현했다고 생각하여 테스트 해도 이상하게 동작한다. 근데 어느 부분이 문제인지도 찾기 힘들다. 본인이 RecyclerView의 동작 원리 및 기초적인 것들을 잘 모르기 때문이라는 생각도 든다. 그래서 RecyclerView의 기초와 프로젝트를 진행하면서 RecyclerView에 대해서 알게 되었던 부분들을 정리하고자 한다.

 

왜 사용하는 거임?

먼저 이 친구를 왜 사용하는 지를 알아야 할 것 같다. RecyclerView는 리스트 형태의 데이터를 화면에 보여줄 때 사용한다. 근데 같은 역할을 할 수 있는 View가 있는데 바로 ListView이다. 그럼 ListView를 사용하지 않고 왜 RecyclerView를 사용할까? 둘의 차이점을 살펴보자.

ListView VS RecyclerView

위의 표를 보면 ViewHolder 패턴의 강제성에 차이점을 볼 수 있다. ViewHolder 패턴에 대해서는 밑에서 설명할 예정이다.

표를 보면 알 수 있듯이 RecyclerView는 ListView보다 장점이 많다. 그냥 단순하게 성능이 더 좋기 때문에 RecyclerView를 사용한다.

RecyclerView가 뭐야?

리스트 형태의 데이터를 뷰과 연결하여 화면에 보여줄 수 있는 뷰이다. 메신저 앱이나 버스 앱 등 스크롤을 내리면서 일정한 형태의 뷰 안에 데이터를 담겨 있는 것들을 RecyclerView로 구현할 수 있다.

이름처럼 뷰를 재활용하는 것이 큰 특징이다. ListView는 화면에서 사라지는 뷰들을 삭제하고 보여줄 뷰들을 생성하기 때문에 큰 비용이 들지만 RecyclerView는 화면 밖으로 사라진 뷰들을 재활용하여 데이터만 갈아 껴서 다시 화면에 보여주기 때문에 큰 비용이 들지 않는다.

RecyclerView 구성 요소

RecyclerView는 adapter, viewHolder와 LayoutManager로 구성된다.

Adapter

adapter는 ViewHolder를 생성하고 데이터를 ViewHolder에 연결하는 역할을 한다.

이를 구현하기 위해서는 RecyclerView.Adapter를 상속 받아서 밑의 메서드들을 오버라이드 해야한다.

  • onCreateViewHolder: ViewHolder를 생성하는 함수이다. 인자로 받는 viewType 형태의 ViewHolder를 화면에 한 번에 보여지는 개수 + 약간의 버퍼 개수(1 ~ 2?) 만큼 생성한다.
  • onBindViewHolder: ViewHolder와 데이터를 연결하는 함수이다. ViewHolder와 position을 인자로 받아서 전달 받은 데이터 리스트의 position의 데이터를 viewHolder에 연결해준다.

ViewHolder

ViewHolder는 화면에 표시될 아이템 뷰를 저장하는 객체이다. 위에서 ListView와 RecyclerView의 차이점을 비교하면서 ViewHolder 사용 강제성의 차이점을 확인할 수 있었다.

ViewHolder 패턴을 사용하지 않고 ListView를 구현하게 되면 adapter를 다음과 같이 만들 수 있다.

class ListViewAdapter(private val items: List<Data>): BaseAdapter() {
    override fun getCount(): Int {
        return items.size
    }

    override fun getItem(position: Int): Any {
        return items[position]
    }

    override fun getItemId(position: Int): Long {
        return 0
    }

    override fun getView(position: Int, p1: View?, parent: ViewGroup?): View {
        val view = LayoutInflater.from(parent?.context).inflate(R.layout.item_recycler, null)
        val titleId = view.findViewById<TextView>(R.id.tv_title)
        val contentId = view.findViewById<TextView>(R.id.tv_content)
        titleId.text = items[position].title
        contentId.text = items[position].content

        return view
    }
}

ListView를 사용하여 아이템을 화면에 보여줄 때 getView() 메서드를 통해서 View를 생성하게 된다. 근데 데이터를 화면에 넣기 위해 findViewById를 사용하는데 이는 비용이 큰 작업이라서 반복적으로 호출하게 되면 성능이 안좋아진다.

이러한 단점을 보완하기 위해서 ViewHolder 패턴을 사용한다.

ViewHolder에 아이템 뷰 담아서 가지고 있기 때문에 findViewById의 중복 호출을 막을 수 있고 이를 통해 성능을 개선시킬 수 있다.

ListView에서 ViewHolder패턴을 적용하려면 직접 구현해야 하지만 RecyclerView에서는 ViewHolder 클래스를 제공한다.

class InfiniteScrollViewHolder(
    private val binding: ItemRecyclerBinding
): RecyclerView.ViewHolder(binding.root) {

    fun bind(item: Data) {
        binding.apply {
            tvTitle.text = item.title
            tvContent.text = item.content
        }
        binding.executePendingBindings()
    }
}

본인은 위와 같이 정의 했다. bind 함수를 통해서 데이터를 뷰에 넣어 줄 수 있게 하였다.

LayoutManager

아이템 뷰들이 정렬되는 방식을 지정하는 역할을 한다.

  • LinearLayoutManager: 수직 or 수평 방향으로 배치
  • GridLayoutManager: 바둑판 모양으로 배치
  • StaggeredGridLayoutManager: 엇갈린 격자 형태로 배치

ListAdpater

RecyclerView.Adapter 클래스를 상속 받아서 Adapter를 구현하여 사용해도 되지만 본인은 ListAdapter를 상속받아서 구현하였다. 이러한 이유는 ListAdapter가 RecyclerView.Adapter보다 장점이 많기 때문이다.

RecyclerView Adapter는 데이터에 변경이 생기면 notifyDataSetChanged() 메서드를 호출하여 전체 리스트를 업데이트한다. 이는 데이터가 1개만 변경되어도 해당되는 작업이다. 500개의 데이터중 하나만 바뀌었는데 이런 작업을 하면 매우 비효율적이다.

“전체를 업데이트 하지 않고 리스트에서 변경된 곳들만 알아서 바꿀 수 있으면 얼마나 좋을까?”

라는 질문의 해결책이 DiffUtil 클래스이다.

DiffUtil

기존의 리스트와 업데이트된 리스트를 비교하여 차이를 알아내는 클래스이다.

리스트의 수만큼 반복하면서 기존의 리스트 요소와 업데이트된 리스트의 요소를 비교하여 바뀌었는지 확인하는 동작을 수행한다.

ex)

class InfiniteScrollDiffCallback : DiffUtil.ItemCallback<Data>() {

    override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
        return oldItem.title == newItem.title
    }

    override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
        return oldItem == newItem
    }
}
  • areItemsTheSame() 메서드는 두 객체가 동일한 항목을 나타내는지 체크한다. (아이템에 별도의 식별 값을 넣어서 판단하면 좋음)
    return false → 데이터 변경!! 내부적으로 아이템을 삭제하고, 새로운 아이템을 insert 하게 된다.
    return true동일 항목!! areContentTheSame() 을 호출하여 두 값이 동일한 이이템인지 확인한다.
    areItemsTheSame의 결과 정보를 기준으로 areContentsTheSame을 한번 더 체크하게 된다
    보통 고유한 값을 가지고 있는 id를 비교한다.

  • areContentsTheSame() : 두 항목의 데이터가 같은지 체크한다. areItemsTheSame() 이 true 일 때만 호출 된다.

하지만 리스트의 요소 개수가 많으면 이를 확인하는 과정에서 많은 시간이 소요될 수 있다. (모든 요소들에 대해서 비교하기 때문) 그래서 이 동작은 백그라운드 스레드에서 처리하는 것을 권장한다.

이는 AsyncListDiffer 클래스로 처리 가능하다.

 

ListAdapter는 ListAdapter<데이터 형식, ViewHolder>(DiffCallback) 형식으로 구현된다.

프로젝트 하면서 배운 점

onBindViewHolder 함수에서 리스너 사용하지 않기

onBindViewHolder 함수에서는 화면에 보여 줄 값을 정의하는 동작만 하는 것이 가장 안정적이다.

ex)

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    Log.d("Adapter Log", "ViewHolder Bind!")
    when (getItem(position).title) {
        "" -> {
            (holder as InfiniteScrollLoadingViewHoler).bind(getItem(position))
        }
        else -> {
            (holder as InfiniteScrollViewHolder).bind(getItem(position))
        }
    }
}

class InfiniteScrollViewHolder(
    private val binding: ItemRecyclerBinding
): RecyclerView.ViewHolder(binding.root) {

    fun bind(item: Data) {
        binding.apply {
            tvTitle.text = item.title
            tvContent.text = item.content
        }
        binding.executePendingBindings()
    }
}

위의 예시처럼 데이터만 정의해주는 것이 좋다.

왜 onBindViewHolder 함수에서 리스너를 호출하면 안좋을까? 다음 로그를 보자

위의 로그는 10개의 데이터를 먼저 넣고 최하단에 닿을 때 마다 데이터가 10개씩 추가되는 RecyclerView의 Adapter의 onCreateViewHolder() 메서드와 onBindViewHolder() 메서드에 로그를 찍고 스크롤을 한 결과이다.

 

onCreateViewHolder() 메서드는 10개의 데이터를 화면에 보여준 이후로 호출되지 않지만 onBindViewHolder() 메서드는 계속 호출되는 것을 확인할 수 있다. 이처럼 onBindViewHolder() 메서드는 반복적으로 호출되기 때문에 여기에서 리스너를 호출하게 되면 리스너가 메모리에서 해제되지 않고 쌓이게 되어 성능 저하를 야기할 수 있다.


 

참고

https://meal-coding.tistory.com/29

https://velog.io/@haero_kim/Android-ViewHolder-패턴을-쓰는-이유

https://velog.io/@kang9366/Android-RecyclerView

https://hungseong.tistory.com/24

https://junyoung-developer.tistory.com/168

https://gift123.tistory.com/67

'Android' 카테고리의 다른 글

컴포즈 공부 2일차  (1) 2023.11.26
컴포즈 공부 1일차 (코드 형식 및 연습)  (1) 2023.11.26
Android CountDownTimer  (0) 2023.11.07
Android Email Intent  (0) 2023.11.07
무한 스크롤  (0) 2023.09.18