안드로이드에서 아이템을 순차적으로 나열하기 위해서 사용할 수 있는 View는 2종류가 있다.
1. ListView
2. RecyclerView
이 둘은 모두 아이템을 화면에 연속적으로 보여주며, 다양한 ViewType을 정의해줄 수 있다.
그러나 몇가지 핵심적인 차이가 존재한다.
RecyclerView와 ListView의 차이점
RecyclerView | ListView | |
ViewHolder | ViewHolder 패턴을 강제한다. | ViewHolder 패턴이 선택 사항이다. |
Scroll & Layout | 수직, 수평 스크롤을 지원한다. Linear, Grid, StaggeredGrid와 같이 아이템 배치를 다양하게 지원한다. |
수직 스크롤만 지원한다. Linear(Vertical) 배치만 가능하다. |
Click Detection | 별도의 클릭 처리 interface를 제공하지 않는다. | AdapterView.OnItemClickListener 인터페이스를 제공한다. |
Decoration | RecyclerView.addItemDecoration() 인자로 RecyclerView.ItemDecoration 클래스의 인스턴스를 전달하여 적용한다. | android:divider, android:dividerHeight 속성을 통해 구분선을 적용한다. |
Animation | Item 추가 / 삭제 Animation을 적용할 수 있다. | Item 추가 / 삭제 Animation을 적용하기 어렵다. |
Speed | Item 개수가 많을 때 빠르다. Item 추가 / 삭제가 빠르다. (notifyItemRangeInserted(), notifyItemRangeChanged(), ListAdapter 등 다양한 갱신 방법 지원) |
Item 개수가 적을 때 빠르다. (단, convertView와 ViewHolder 패턴을 적절히 사용하면 RecyclerView 속도에 준할 수 있다.) Item 추가 / 삭제가 느리다. (오직 notifyDatasetChanged() 지원) |
1. ViewHolder
public abstract static class Adapter<VH extends ViewHolder> {
...
@NonNull
public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);
public abstract void onBindViewHolder(@NonNull VH holder, int position);
...
}
RecyclerView는 ListView와 다르게 ViewHolder 패턴을 강제한다.
따라서 RecyclerView.Adapter에서는 아래와 같은 추상 메서드 구현이 필요하다.
onCreateViewHolder()는 RecyclerView를 통해 사용자에게 한 번에 보여지는 item의 2 ~ 4개를 더한만큼만 호출된다.
데이터가 10,000개, 100,000개라 할지라도 onCreateViewHolder()는 같은 화면 크기에 대해 호출 횟수가 고정되어 있다.
다시 말해, RecyclerView는 동일한 View를 재사용하고, 새로운 item이 보여질 때마다 onBindViewHolder()를 호출하여 같은 View에 데이터만 갈아끼우는 방식이다.
따라서 동일한 View에 대해 매번 자식 View를 조회하는 것은 불필요하다.
뿐만 아니라, findViewById()는 주어진 id를 바탕으로 유효한 id인지 검증하고, 유효하다면 findViewTraversal()을 호출한다.
findViewTraversal()은 일반적인 View라면, 자신의 id와 동일한지 비교하고 종료된다.
그러나 ViewGroup의 경우, 모든 자식 View를 순회하기 때문이 cost가 높다.
이것이 RecyclerView가 ViewHolder 패턴을 강제하는 이유이다.
한 번 조회한 View에 대해 ViewHolder로 감싸서 관리하면, 동일한 View를 재사용할 때 매번 View를 조회할 필요가 없다.
반면, ListView는 ViewHolder 패턴을 강제하지는 않는다.
일례로 BaseAdapter의 최상위 interface인 Adapter의 코드는 다음과 같다.
public interface Adapter {
...
View getView(int position, View convertView, ViewGroup parent);
...
}
ListView도 RecyclerView와 동일하게 한 번 생성한 view를 재사용할 수 있다.
이러한 view를 convertView라고 한다.
이미 view가 생성된 적이 있다면 껍데기 view에 데이터만 갈아 끼워주면 된다.
@SuppressLint("ViewHolder")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = View.inflate(parent?.context, R.layout.item_movie, null)
...
}
getView() 메서드를 구현한 위 코드는 ViewHolder warning이 발생한다.
convertView가 이미 생성되었다면 null이 아닐 것이고, 새롭게 View를 inflate 할 필요가 없다.
또한, ListView의 Adapter는 onBindViewHolder()와 같이 데이터를 bind하는 메서드가 따로 없다.
view 생성과 data bind를 getView()라는 메서드 하나에서 처리해야 한다는 의미이다.
// Adapter
class MyListAdapter(...) : BaseAdapter() {
...
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var view: View? = convertView
val viewHolder: ViewHolder
if (view == null) {
val view = View.inflate(parent?.context, R.layout.item_movie, null)
viewHolder = ViewHolder(view)
view.tag = viewHolder
} else {
viewHolder = view.tag as ViewHolder
}
viewHolder.bind(...)
return view
}
...
}
// ViewHolder
private class ViewHolder(view: View) {
private val nameTextView: TextView = view.findViewById(...)
private val phoneNumberTextView: TextView = view.findViewById(...)
...
fun bind(...) {
nameTextView.text = ...
phoneNumberTextView.text = ...
...
}
}
getView() 메서드가 호출될 때, convertView가 null인 경우 새롭게 View 객체를 초기화해야 한다.
(만약 이미 초기화된 View라면 view의 tag에 저장한 ViewHolder를 가져오기만 하면 된다.)
새롭게 초기화한 View 객체를 ViewHolder로 넘겨서 필요한 자식 View를 초기화하여 관리한다.
이러한 과정을 통해 ListView도 RecyclerView처럼 findViewById()를 한 번만 호출해주도록 구현할 수 있다.
다만, 이러한 과정이 선택 사항이므로 개발자가 구현하는 과정에서 빠뜨릴 수도 있다.
또한 모든 과정을 getView() 메서드에서 처리해야 하므로 가독성이 떨어질 수 있으며, 일일이 convertView의 null 체크와 ViewHolder를 직접 별도로 관리해야 한다는 점에서 많은 불편함을 가지고 있다.
2. Scroll & Layout
ListView는 수직 스크롤만 지원한다.
일반적인 방법으로는 수평 스크롤링을 구현할 수 없다는 의미이다.
반면, RecyclerView는 Vertical, Horizontal 2가지를 선택할 수 있으며 각각 수직, 수평 스크롤을 지원한다.
또한 ListView와 다르게 item을 다양하게 배치시킬 수 있다.
RecyclerView의 LayoutManager에 따라 item 배치를 다양하게 정의할 수 있다.
- LinearLayout
- View를 일직선으로 배치한다.
- GridLayout
- View를 격자 형태로 배치한다.
- 마치 TableLayout과 비슷하며 Row, Column의 수를 설정하여 원하는 형태로 보여줄 수 있다.
- StaggeredGridLayout
- GridLayout과 유사하지만, View의 높이를 불규칙하게 나타낼 수 있다.
3. Click Detection
ListView(this).setOnItemClickListener { adapterView, itemView, position, id ->
val item = adapterView.getItemAtPosition(position)
// logic for clicking itemView
...
}
ListView는 AdapterView.OnItemClickListener 인터페이스를 구현하여 View가 클릭되었을 때에 대한 처리를 수행할 수 있다.
하지만 RecyclerView는 별도로 Item 클릭에 대한 인터페이스를 제공하지 않는다.
따라서 View.OnClickListener를 onCreateViewHolder() 메서드가 호출될 때 View에 직접 등록해주어야 한다.
(만약 View 클릭에 대해 테스트 가능한 구조를 원한다면, 외부에서 Click 로직을 주입받는 방식을 택한다.)
4. Decoration
RecyclerView는 각 Item에 장식을 추가하기 위해 RecyclerView.addItemDecoration()을 호출해야 한다.
ItemDecoration은 추상 클래스이므로 원하는 형태로 커스터마이징이 가능하다.
만약 단순히 구분선을 추가하고 싶다면, 이미 구현되어 있는 DividerItemDecoration 인스턴스를 전달한다.
ListView는 Item에 구분선을 추가하기 위해, android:divider 및 android:dividerHeight를 설정해주면 된다.
divider는 drawable을 전달받으며, dividerHeight은 구분선의 높이를 지정할 수 있다.
동적으로 decoration을 추가하는 RecyclerView와 달리 xml의 속성만으로도 설정이 가능하다.
5. Animation
ListView는 Animation을 추가하는 것이 가능하나 매우 어렵고 무거운 작업이다.
각 View에 대해 AnimationUtils.xxx 등을 통해 직접 등록해주어야 하며 Item의 추가/ 삭제에 대한 상황에 Animation이 동작하도록 별도로 구현해주어야 한다.
반면, RecyclerView는 아이템 추가 / 삭제시 비교적 쉽게 Animation 적용이 가능하다.
android:layoutAnimation에 animation 리소스에 해당하는 xml 파일을 전달해주기만 하면 된다.
참고로, LayoutAnimation은 RecyclerView에만 적용되는 개념이 아니다.
LayoutAnimation은 ViewGroup의 자식 View에게 일괄적으로 animation을 적용한다.
기본적으로 자식 View들이 모두 그려졌을 때 1회 호출되며, 재실행이 불가능하다.
+ ) notifyDatasetChanged()와 같이 RecyclerView의 Item들을 업데이트 하는 경우, 이미 View에 대해 animation 효과를 처리하였으므로 재실행 되지 않을 수 있다.
이때 다음 코드로 문제를 해결할 수 있다.
adapter.notifyDatasetChanged()
recyclerView.scheduleLayoutAnimation()
ViewGroup의 scheduleLayoutAnimation() 를 호출하여, ViewGroup의 contents가 업데이트 될 때동일한 View에 대해서도 애니메이션이 재동작하도록 설정할 수 있다.
Schedules the layout animation to be played after the next layout pass of this view group. This can be used to restart the layout animation when the content of the view group changes or when the activity is paused and resumed.
'Android' 카테고리의 다른 글
[Android] PendingIntent 공식문서 파헤치기 (0) | 2023.05.02 |
---|---|
[Android] RecyclerView Animation (LayoutAnimation, ItemAnimator) (0) | 2023.04.28 |
[Android] This version of the Android Support plugin for IntelliJ IDEA (or Android Studio) 오류 해결 (2) | 2022.12.23 |
[Android] ViewModel + Event Wrapper Pattern 단일 이벤트 처리 (0) | 2022.12.12 |
[Android] Firebase Dynamic Link를 활용하여 사용자 초대링크 생성하기 (2) | 2022.02.08 |