-
[Android] 우리가 RecyclerView를 사용하는 이유 (feat. ListView)개발/Android 2025. 1. 10. 18:47
들어가며
RecyclerView는 ListView의 단점을 보완하기 위해 사용하는 ViewGroup이며, 여러 아이템들을 목록으로 보여주는 역할을 합니다.
Android 개발을 조금 할 줄 알게된 지금은 RecyclerView를 사용하는 것이 익숙해졌고, 또 RecyclerView의 사용을 너무나 당연하게 생각하고 있었습니다.
하지만 사용하는 기술의 특성이 무엇이며, 왜 사용하는지 알고서 활용하는 것이 정말 중요합니다.
이번 포스팅의 목적은 ListView와 RecyclerView가 무엇이고, 또 RecyclerView를 사용하는 이유가 무엇인지 다시 상기하는 것입니다.
ListView의 특성과 단점을 먼저 파악하고, RecyclerView의 특징과 ListView와 비교했을 때의 장점을 알아보겠습니다.
포스팅은 아래의 순서로 진행됩니다.
목차
ListView가 무엇인가요?
- ListView의 특징
- ListView의 단점
그럼 RecyclerView는 무엇이죠?
- RecyclerView 란?
- ListView와 RecyclerView 비교
- ListView에서 RecyclerView로!
ListView가 무엇인가요?
RecyclerView를 알아보기 전에, RecyclerView와 자주 비교되는 ListView에 대해 자세히 알아볼 필요가 있습니다. ListView의 특성부터 천천히 살펴보겠습니다.
ListView는 이름 그대로 View들의 집합을 리스트의 형태로 보여주는 View입니다.
안드로이드 공식 문서에서는 아래와 같이 소개하고 있습니다.
ListView 공식문서 설명
Displays a vertically-scrollable collection of views, where each view is positioned immediately below the previous view in the list.ListView는 세로로 스크롤 가능한 view들의 집합을 나타내며, 리스트에서 각 view들은 직전 view 바로 아래에 위치합니다.
즉, 데이터 집합을 각각의 아이템이 담긴 리스트의 형태로 나타내는 View입니다. 아이템(view)을 수직, 선형적으로 나열해주며, 스크롤을 하여 다른 위치의 아이템(view)에도 접근이 가능한 ViewGroup 입니다.
* 해당 포스팅에서는 ListView, RecyclerView 내부에 나타나는 view를 편의 상 아이템으로 지칭하겠습니다.
정말 자주 사용되는 ListView의 형태 ListView는 위와 같은 형태의 View를 만들어줍니다. 서비스는 다양한 아이템 정보를 리스트의 형태로 효과적으로 나타낼 수 있고, 사용자는 스크롤로 여러 정보들을 편리하게 확인할 수 있습니다.
ListView의 특징
ListView의 대표적인 특징은 아래와 같습니다.
- 아이템 목록을 수직 방향으로만 보여줄 수 있다.
- ListView는 adapter view이다.
- View를 재사용한다.
위의 세 가지 특징들에 대해 살펴보겠습니다.
아이템 목록을 수직 방향으로만 보여줄 수 있다.
위에서 설명한대로, ListView는 아이템 목록을 수직 방향으로만 나열하여 보여줍니다.
만일 2개 이상의 열로 나누어 보여주거나, 가로 방향으로 나열하여 보여주고 싶다면 ListView가 아닌 다른 View를 사용해야 합니다.
ListView는 adapter view이다.
ListView 공식문서의 설명 중
A list view is an adapter view that does not know the details, such as type and contents, of the views it contains.사실 ListView는 리스트에 담기는 아이템의 세부사항을 모릅니다. 해당 아이템이 어떤 타입이고, 어떤 내용을 담고있는지 알지 못합니다.
ListView가 아이템을 모른다면 어떻게 그 목록을 화면에 보여주는 것일까요?
ListView는 Adapter를 사용합니다. Adapter는 Interface로, ListView를 대신해서 아이템 View들의 정보와 상태를 관리합니다.
ListView가 아이템 목록을 화면에 보여주는데 필요한 정보들을 Interface로 제공해주며, ListView는 이 Adapter를 이용하여 화면에 리스트를 나타냅니다.
ListView 뿐 아니라 GridView, 잠시 뒤 살펴볼 RecyclerView 또한 이 Adapter를 이용합니다.
Adapter 공식문서의 설명
An Adapter object acts as a bridge between an AdapterView and the underlying data for that view. The Adapter provides access to the data items. The Adapter is also responsible for making a (android.view.View) for each item in the data set.Adapter를 사용하는 AdapterView(ListView, RecyclerView 등)와 화면에 보여줄 데이터 목록 사이의 연결다리 역할을 한다.
ListView는 주로 ListAdapter를 상속한 BaseAdapter()를 이용합니다.
공식 문서에서 제공한 예제 코드를 바탕으로 조금 더 자세히 알아보겠습니다.
class MyAdapter( private val items: List<String>, ) : BaseAdapter() { // 어댑터가 가진 아이템의 개수를 반환 override fun getCount(): Int = items.size // 해당 위치(position)의 아이템 데이터를 반환 override fun getItem(position: Int): String = items[position] // 해당 위치(position)의 아이템 데이터의 ID를 반환 override fun getItemId(position: Int): Long = position.toLong() // 해당 위치(posision)의 아이템 View를 반환 override fun getView( position: Int, convertView: View?, parent: ViewGroup?, ): View { if (convertView == null) { convertView = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false) } // 각 위치의 item에 따라 View 상태를 지정합니다. convertView?.findViewById(R.id.text1)?.text = getItem(position) return convertView } }
공식 문서의 자바 코드를 코틀린으로 변경하고, getView(...) 메서드 이외의 다른 오버라이드 메서드도 함께 넣었습니다.
위의 코드가 일반적으로 볼 수 있는 BaseAdapter의 형태입니다.
자세한 view 구현 부분은 예시일 뿐이니 넘어가고, 각 오버라이드 메서드의 역할을 간단히 살펴보겠습니다.
- getCount() : Adapter가 가지고 있는 아이템의 개수를 반환합니다.
- getItem(position: Int) : 해당 위치(position)의 데이터를 반환합니다.
- getItemId(position: Int) : 해당 위치(position)의 데이터 ID를 반환합니다.
- getView(position: Int, convertView: View, parent: ViewGroup) : 해당 위치(position)의 아이템 View를 생성하여 반환
즉, ListView가 위 BaseAdapter의 interface를 활용하여 각각의 아이템 View를 그려낼 수 있는 것입니다.
특히 getView() 메서드가 중요합니다.
Adapter의 getView() 메서드에서는 특정 위치에 보여줄 아이템 view를 반환해줍니다.
아이템을 화면에 보여줄 수 있도록 view를 inflate하고, 해당 view에 필요한 데이터를 끼워 넣어줍니다.
그렇게 만들어낸 아이템 view를 ListView가 받아서 사용합니다.
inflate란?
'부풀리다'라는 뜻으로, 안드로이드에서는 View 객체를 인스턴스화하여 만들어내는 동작을 의미합니다.
위의 예제 코드에서는 LayoutInflater를 사용하여 View 객체를 만들어내고 있습니다.이 getView 메서드는 ListView의 단점과도 연관되어 있는데요, 자세한 설명은 잠시 뒤에 다루도록 하겠습니다.
View를 재사용한다.
또, ListView는 내부적으로 view 객체를 재사용하기 위한 노력을 하고 있습니다.
기기의 메모리 공간은 한정되어있고, 스크롤을 할 때마다 나타나는 아이템들을 매번 그려낸다면 메모리 자원이 낭비될 것입니다.
ListView의 코드를 살펴보면, RecyclerBin 객체를 이용해 view 객체를 저장하고 재사용하는 동작이 나타나있습니다. (아래 코드블럭 참고)
지금 상세히 알아야하는 내용은 아닙니다. 그렇구나 정도로 이해하고 넘어가셔도 좋습니다.
public class ListView extends AbsListView { /* ... */ @Override protected void layoutChildren() { /* ... */ try { /* ... */ // Pull all children into the RecycleBin. // These views will be reused if possible final int firstPosition = mFirstPosition; final RecycleBin recycleBin = mRecycler; if (dataChanged) { for (int i = 0; i < childCount; i++) { recycleBin.addScrapView(getChildAt(i), firstPosition+i); } } else { recycleBin.fillActiveViews(childCount, firstPosition); } /* ... */ } /* ... */ } /* ... */ @UnsupportedAppUsage private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { if (!mDataChanged) { // Try to use an existing view for this position. final View activeView = mRecycler.getActiveView(position); if (activeView != null) { // Found it. We're reusing an existing child, so it just needs // to be positioned like a scrap view. setupChild(activeView, position, y, flow, childrenLeft, selected, true); return activeView; } } // Make a new view for this position, or convert an unused view if // possible. final View child = obtainView(position, mIsScrap); // This needs to be positioned and measured. setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; } /* ... */ }
혹시나 자세한 구현이 궁금하시다면 아래를 참고하시기 바랍니다.
참고 : ListView 내부 코드, AbsListView 내부 코드
(자세한 이야기는 추후 별도의 포스팅으로 다뤄보겠습니다!)
ListView의 단점
ListView의 특성을 살펴보았습니다. 설명만 들어서는 사용하는데 큰 문제가 없어보이는데요, 하지만 크게 아래와 같은 단점이 존재합니다.
- 아이템 목록을 수직 방향으로 밖에 보여주지 못한다.
- 구현에 따라 View를 효율적으로 사용하지 못하게 되며, 성능이 저하될 수 있다.
아이템 목록을 수직 방향으로 밖에 보여주지 못한다.
세로로 나열된 목록을 보여준다면 충분하지만, 만약 다른 형태로 보여주고자 한다면 다른 적합한 View를 사용해야 합니다.
UI는 변경사항이 자주 일어나는 부분입니다. 아이템들을 보여주는 방향이 가로로 변경될수도, 또는 그리드 형태로 변경될 수도 있습니다.
어쨌거나 View를 구성하는 코드의 변경은 피할 수 없지만, ListView가 아닌 다른 View를 적용해야하기 때문에 코드의 변경이 커질 수 있습니다.
구현에 따라 View를 효율적으로 사용하지 못하게 된다.
View 객체를 재사용하여 성능을 높이기 위해서는 개발자의 노력이 필요합니다.
아래 예제 코드를 살펴볼까요?
class MovieListAdapter( private val movieList: List<Movie>, ) : BaseAdapter() { override fun getCount(): Int = movieList.size override fun getItem(index: Int): Movie = movieList[index] override fun getItemId(index: Int): Long = index.toLong() override fun getView( index: Int, convertView: View?, parent: ViewGroup, ): View { val view: View = LayoutInflater.from(parent.context).inflate(R.layout.movie_item, parent, false) val movie = movieList[index] view.findViewById(R.id.movie_title).text = movie.title view.findViewById(R.id.movie_screening_date).text = movie.screeningDate view.findViewById(R.id.movie_running_time).text = movie.runningTime return view } }
상영중인 영화 목록을 보여주는 화면을 구현 중이라고 가정해봅시다.
화면에서 영화 목록을 ListView로 보여주고, 해당 ListView가 사용하는 Adapter를 구현했습니다.
하지만, 위 코드는 2가지 문제점을 지니고 있습니다. 어떤 문제점이 있을까요?
View 를 재사용하고 있지 않다.
첫째로, 위 코드는 view를 재사용하고 있지 않습니다.
앞서 중요하다고 짚었던 BaseAdapter()의 getView() 메서드를 공식 문서의 코드와 비교하여 다시 살펴봅시다.
override fun getView( position: Int, convertView: View?, parent: ViewGroup, ): View { if (convertView == null) { convertView = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false) } convertView?.findViewById(R.id.text1)?.text = getItem(position) return convertView }
눈치 채셨나요? 공식 문서에서는 getView() 메서드의 파라미터로 들어오는 convertView에 대한 null check가 이루어지고 있습니다.
하지만 MovieListAdapter의 getView() 메서드 구현은 그렇지 않습니다.
ListView 공식 문서에서도 view를 재사용하기 위해서는 convertView에 대한 null check를 하라고 설명하고 있습니다.
ListView attempts to reuse view objects in order to improve performance and avoid a lag in response to user scrolls. To take advantage of this feature, check if the convertView provided to getView(...) is null before creating or inflating a new view object.
convertView가 무엇이기에 null check를 하는 것일까요?
convertView 에 대한 설명
The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view. Heterogeneous lists can specify their number of view types, so that this View is always of the right type (see getViewTypeCount() and getItemViewType(int)).getView()에 대한 공식 문서의 설명에 따르면, 재사용에 활용하는 old view라고 합니다.
즉 convertView는 이전에 사용되었던 아이템 view 객체를 의미합니다.
앞에서 ListView가 내부적으로 View(아이템)를 재사용한다고 했습니다. ListView는 처음에 Adapter가 inflate 하여 만들었던 아이템 view 객체를 재사용하기 위해서, getView() 메서드의 파라미터로 넘겨줍니다. Adapter는 view 객체인 convertView를 넘겨받아 필요한 데이터만 집어넣어 반환해줍니다. 이렇게 새로운 아이템 view 객체를 만들지 않고 이전에 사용했던 view 객체를 재사용할 수 있습니다.
만일 convertView가 null이라면 재사용 가능한 view 객체가 없다는 것을 의미하고, 이 경우 새로 view 객체를 생성하여 보여주어야하므로 inflate 작업이 필요합니다.
그러므로 view 객체를 재사용하기 위해서는 convertView가 null인지 확인하고, null이라면 새로운 view 객체를 inflate하는 구현이 이루어져야 합니다.
아래와 같이 convertView를 null check 하도록 코드를 변경한다면 이전에 사용되었던 view 객체를 재사용할 수 있습니다.
class MovieListAdapter( private val movieList: List<Movie>, ) : BaseAdapter() { override fun getCount(): Int = movieList.size override fun getItem(index: Int): Movie = movieList[index] override fun getItemId(index: Int): Long = index.toLong() override fun getView( index: Int, convertView: View?, parent: ViewGroup, ): View { // convertView 재사용 시도, null이라면 새로운 아이템 view를 inflate val view: View = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.movie_item, parent, false) val movie = movieList[index] view.findViewById(R.id.movie_title).text = movie.title view.findViewById(R.id.movie_screening_date).text = movie.screeningDate view.findViewById(R.id.movie_running_time).text = movie.runningTime return view } }
findViewById() 의 호출이 자주 일어난다.
view 객체의 재사용에도, 위의 코드는 여전히 성능 상의 문제점이 남아있습니다.
바로 findViewById() 메서드로 View에 접근하여 데이터를 수정하는 구현 때문입니다.
우리가 바라보는 화면은 여러 View가 트리 구조로 이루어진 뷰 계층 구조로 나타나 있습니다.
findViewById() 메서드는 ID와 일치하는 View를 뷰 계층 구조의 root에서부터 찾아서 반환해줍니다.
만약 복잡한 View 구조를 가진 화면이라면, 그만큼 트리의 Depth가 높고, View를 찾는데 걸리는 시간이 오래 걸립니다.
이처럼 findViewById()는 꽤나 무거운 작업입니다. 하지만 위의 코드에서는 findViewById()를 매번 호출하여 아이템의 구성요소(TextView, Button, ImageView 등)를 업데이트합니다.
ListView가 아이템을 보여줄 때 마다 getView()가 호출되고, 그때마다 뷰 계층 구조에서 일일이 구성요소들을 탐색하는 비용이 발생하는 것이죠.
개선 방법 : ViewHolder 패턴 적용
그러므로 findViewById()의 호출 빈도를 최소한으로 줄이는 것이 성능 향상을 이끌어낼 수 있습니다.
이를 위해 우리는 ViewHolder 패턴을 활용할 수 있습니다.
ViewHolder 패턴이란?
우선 ViewHolder를 직역하면 View의 껍데기라는 뜻입니다.
각 아이템 View를 담는 객체이며, View의 구성요소을 저장합니다. 화면에 새로운 아이템을 보여줄 때 마다 반복적으로 findViewById()를 이용해 아이템의 구성요소에 접근할 필요 없이, 구성요소가 저장된 ViewHolder를 사용할 수 있습니다.
즉, 구성요소를 직접 찾아서 접근하지 않고, 구성요소의 참조가 저장된 ViewHolder를 다시 사용하는 것입니다. 이로써 findViewById()의 호출 횟수를 줄일 수 있습니다. 이러한 ViewHolder를 사용하는 것을 ViewHolder 패턴이라고 부릅니다.
ViewHolder는 코드에서 아래와 같이 정의할 수 있습니다.
data class MovieViewHolder( val title: TextView, val screeningDate: TextView, val runningTime: TextView, ) { fun bindData(movie: Movie) { title.text = movie.title screeningDate.text = movie.screeningDate runningTime.text = movie.runningTime.toString() } companion object { fun of(view: View): MovieViewHolder = MovieViewHolder( view.findViewById(R.id.title), view.findViewById(R.id.screeningDate), view.findViewById(R.id.runningTime), ) } }
ViewHolder에 담겨야할 View 구성요소를 프로퍼티로 정의하고, findViewById()로 구성요소를 찾아 생성할 수 있도록 팩토리 메서드 of()를 동반 객체로 지정했습니다.
bindData()는 ViewHolder의 구성요소 데이터를 설정 및 변경할 때 사용합니다.
위에서 정의한 ViewHolder를 BaseAdapter()의 getView()에서 사용하는 코드입니다. View의 tag 속성을 활용하여 ViewHolder를 지정할 수 있습니다.
override fun getView( index: Int, convertView: View?, parent: ViewGroup, ): View { // 재사용 가능한 View가 없다면(convertView가 null) 새로운 아이템 View를 inflate 합니다. val view: View = convertView ?: inflateView(parent) val movieViewHolder: MovieViewHolder if(convertView == null) { // convertView가 null인 경우 ViewHolder 객체를 생성합니다. movieViewHolder = MovieViewHolder.of(view) // 생성한 ViewHolder를 View의 tag로 설정합니다. view.tag = movieViewHolder } else { // convertView가 null이 아니라면, 기존의 ViewHolder를 재사용합니다. movieViewHolder = view.tag as MovieViewHolder } // index 위치의 영화 정보를 ViewHolder에 끼워넣습니다. val movie = movieList[index] movieViewHolder.bindData(movie, listener) return view } // 아이템 view를 inflate하는 함수입니다. private fun inflateView(parent: ViewGroup): View { return LayoutInflater.from(parent.context).inflate(R.layout.movie_item, parent, false) }
이렇게 ViewHolder 패턴을 적용하여 findViewById()의 호출 횟수를 줄일 수 있으며, ListView의 성능을 향상시킬 수 있습니다.
그럼 RecyclerView는 무엇이죠?
위에서 살펴본 ListView의 성능 개선 방법이 있음에도 불구하고, 여전히 해결되지 않는 문제점들이 존재합니다.
- 아이템의 애니메이션 적용이 복잡하다.
- 리스트에 다양한 아이템 타입(ViewType)이 존재할 경우, 이 역시 커스텀 처리가 어렵다.
- 만약 영화 목록 사이에 광고를 보여주어야 한다면, ViewType에 따라 다른 아이템 View를 반환하도록 변경해주어야 합니다.
- 하지만 ListView는 이에 대한 처리가 복잡합니다.
- 세로 방향으로 밖에 보여주지 못한다.
- 성능 향상을 위해서는 개발자가 직접 convertView에 대한 처리를 해주어야 하며, ViewHolder 패턴을 따로 적용해주어야 한다.
- 이 작업은 번거롭고 보일러 플레이트 코드를 만들어내어 생산성을 저하시킬 수 있습니다.
이 문제점들을 해결하기 위해 RecyclerView가 등장했습니다.
RecyclerView란?
이름에서 알 수 있듯, View을 재활용하여 데이터 목록을 보여주는 ViewGroup입니다.
A flexible view for providing a limited window into a large data set.
규모가 큰 데이터 집합을 제한된 화면에서 나타내주는 유연한 View 입니다.
RecyclerView의 특징
ListView와 동일하게 데이터 집합을 리스트의 형태로 보여주지만, ListView의 몇 가지 문제점들을 개선하였습니다.
- ViewHolder 패턴을 강제화합니다.
- ViewHolder를 강제로 사용하게 만들어 View의 재사용성을 높이고, 메모리를 효율적으로 사용할 수 있습니다.
- 부드러운 애니메이션을 지원해줍니다.
- 아이템 추가, 삭제, 변경에 대해서 부드러운 애니메이션을 기본으로 제공해줍니다.
- 다양한 레이아웃 종류를 지원해줍니다.
- Linear, Grid 등의 다양한 레이아웃을 설정할 수 있으며, 가로 방향으로도 전환이 가능합니다.
- 또한 ViewType에 따라 다양한 아이템들을 구분하여 보여줄 수 있습니다.
- ListView와 달리 아이템을 ViewType에 따라 구분할 수 있으며 구현도 어렵지 않습니다.
ListView와 비교하여 RecyclerView가 가진 장점들은 다음과 같습니다.
구분 ListView RecyclerView ViewHolder 패턴의 강제화 선택 강제 성능 데이터가 적을 때 효율적 데이터가 많을 때 효율적 아이템 추가/삭제 느림 빠름 아이템 레이아웃 세로 고정 LayoutManager 지원 및 orientation 설정 가능(가로/세로) 애니메이션 전용 클래스 지원하지 않음 전용 클래스 지원 ViewType 지원 X, 직접 구현이 필요하며 불안정 지원 O, onCreateViewHolder()에서 ViewType 구분 이렇듯 ViewHolder 사용의 강제로 성능의 향상을 이끌어내고, 애니메이션, 아이템 레이아웃 지원 등 다양한 기능을 추가로 제공해주어 사용성을 높여주었습니다.
안드로이드의 공식 문서에서도 ListView보다는 RecyclerView의 사용을 권장하고 있습니다.
ListView 공식 문서의 설명 중
For a more modern, flexible, and performant approach to displaying lists, use RecyclerView.
ListView에서 RecyclerView로!
RecyclerView의 특징에 대해 알아보았으니, 코드에서는 어떻게 사용하는지 간단하게 살펴보겠습니다.
위의 영화 목록을 ListView로 보여주던 코드 예제를 RecyclerView를 적용하여 나타내보겠습니다.
잠깐!
RecyclerView를 사용하기 위해서는 모듈 수준의 build.gradle.kts 파일에 의존성을 추가해야합니다.
dependencies { implementation("androidx.recyclerview:recyclerview:1.3.2") }
XML 코드
우선 RecyclerView를 사용하기 위해, xml 파일에서 ListView 태그를 RecyclerView 태그로 변경합니다.
<androidx.constraintlayout.widget.ConstraintLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/movieList" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/movie_item"/> </androidx.constraintlayout.widget.ConstraintLayout>
다음으로 아이템 리스트를 보여줄 레이아웃을 지정하는 LayoutManager를 설정합니다.프로그래밍적으로도 LayoutManager의 설정이 가능하지만, 아래와 같이 xml 태그의 속성으로도 지정이 가능합니다.
여기서는 세로 방향으로 나열하여 나타낼 것이기에 LinearLayoutManager로 설정하였습니다.
<androidx.recyclerview.widget.RecyclerView android:id="@+id/movieList" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/movie_item"/>
만일 가로 방향으로 나열하고 싶다면, RecyclerView의 orientation 속성을 "horizontal"로 변경하면 됩니다.
<androidx.recyclerview.widget.RecyclerView android:id="@+id/movieList" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:listitem="@layout/movie_item"/>
RecyclerView.ViewHolder
RecyclerView는 ViewHolder 패턴을 강제한다고 했습니다.
따라서 RecyclerView에서 제공해주는 RecyclerView.ViewHolder 를 이용합니다.
data class MovieViewHolder(private val itemView: View) : RecyclerView.ViewHolder(itemView) { val title: TextView = itemView.findViewById(R.id.title) val screeningDate: TextView = itemView.findViewById(R.id.screeningDate) val runningTime: TextView = itemView.findViewById(R.id.runningTime) fun bindData(movie: Movie) { title.text = movie.title screeningDate.text = movie.screeningDate runningTime.text = movie.runningTime.toString() } }
Adapter 코드
RecyclerView는 ListView에서 사용했던 BaseAdapter()가 아닌, RecyclerView.Adapter()를 사용합니다.
코드 예제부터 먼저 살펴볼까요?
class MovieListAdapter( private val movieList: List<Movie>, ) : RecyclerView.Adapter<MovieViewHolder>() { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, ): MovieViewHolder { val view: View = LayoutInflater.from(parent.context).inflate(R.layout.movie_item, parent, false) return MovieViewHolder(view) } override fun onBindViewHolder( holder: MovieViewHolder, position: Int, ) { holder.bindData(movieList[position]) } override fun getItemCount(): Int = movieList.size }
코드가 더욱 깔끔해지고 간결해졌습니다. 특히 BaseAdapter()의 getView()에 있던 아이템 view 객체를 inflate하는 코드와 데이터를 설정하는 코드가 두 개의 메서드로 나뉘어진 것을 확인할 수 있습니다.
위에서 알 수 있듯, RecyclerView의 Adapter는 세 가지 오버라이드 메서드를 구현해야 합니다.
- onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
- ViewHolder를 인스턴스화(inflate) 하여 반환합니다.
- onBindViewHolder(holder: ViewHolder, position: Int)
- ViewHolder에 필요한 데이터를 bind 합니다. (아이템 View의 데이터를 설정하는 작업)
- getItemCount(): Int
- 전체 아이템의 개수를 반환합니다.
마치며
지금까지 RecyclerView 이전에 사용되었던 ListView의 특징과 단점, ListView의 단점을 보완하는 RecyclerView의 특징을 알아보았습니다. 그러면서 RecyclerView를 사용하는 이유에 대해서 이해해보았습니다.
사실 저는 RecyclerView에 대해 다시 알아보기 전까지, ListView는 View를 재사용할 수 없기 때문에 RecyclerView를 사용해야한다고 알고 있었습니다. 하지만 ListView도 View의 재사용이 가능하며, 성능을 향상시킬 수 있었습니다. 그럼에도 위에서 설명한 문제점들이 남아있기 때문에 RecyclerView의 사용을 권장하는 것이었습니다.
사용하려는 것이 단순히 더 좋은 것이고, 공식 문서에서 권장하기 때문이라고만 알고 넘어가기보다는, 기존의 것이 어떤 특성과 문제점을 가지고 있었기에 현재의 기술을 사용하는 것인지 정확하게 알고 넘어가는 것이 중요한 것 같습니다.
이번 포스팅을 작성하며 저는 아직 모르는 것이 많음을 다시 한 번 느끼고, 꾸준한 공부가 필요함을 상기하게 되었습니다.
'개발 > Android' 카테고리의 다른 글
[Android] Github Actions로 CI 적용하기 - 간단 가이드 (0) 2025.04.02 [Android] Compose 상태 관리 심화 (0) 2025.02.15 [Android] Compose 상태 관리의 기본 개념 (0) 2025.01.30