신입 개발자를 위한 Repository를 만들었습니다. 공부한 내용을 정리 중이니 도움이 되신다면 와서 Star를 눌러주시면 감사하겠습니다.

우리가 사용하는 앱 중에서 가장 많이 등장하는 것은 무엇일까?? 그 중의 하나는 바로 리스트라고 해도 과언이 아닐 것이다. 사용자에게 리스트 즉, 목록을 보여주는 것이 가장 많이 등장한다. 그렇다면 이 리스트를 구현하는 방법에는 무엇이 있을까??

ListView

ListView는 이름에서도 알 수 있듯이 리스트 즉, 목록을 구현하는데 사용된다. ListView는 안드로이드에 임베디드 되어 있는 코드로 동작하며, API level 1부터 존재했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
Holder holder = new Holder();
View rowView = inflater.inflate(R.layout.item_list, null);
holder.tv = (TextView) rowView.findViewById(R.id.text);
holder.img = (ImageView) rowView.findViewById(R.id.image);
holder.tv.setText(result[position]);
holder.img.setImageResource(imageId[position]);
rowView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Toast.makeText(context, "You Clicked " + result[position], Toast.LENGTH_LONG).show();
}
});
return rowView;
}

위의 형태는 가장 일반적인 ListView의 getView() 접근 방법이다. 하지만 위와 같이 동작하게 되면 getView() 즉, ListView의 재사용성이 떨어지게 된다.

재사용이라는게 getView()는 현재 화면상에 아이템이 보일 때 호출되는 함수이다. 예를 들어, 아이템이 20개가 있고 이를 스크롤한다고 가정하면 스크롤 시에도 getView() 함수는 계속해서 호출된다.

1
2
Holder holder = new Holder();
View rootView = inflater.inflate(R.layout.item_list, null)

또한, 위의 코드는 별도의 null 처리가 없으므로 스크롤을 할 때마다 inflate를 통해서 View의 create가 발생하고 findViewById도 함께 호출된다. [이러한 부분들이 비효율적이다.] 리스트의 특성상 하나의 View만 있으면 이 View가 연속적으로 사용이 가능한 형태가 만들어지면 되는데 ListView는 이러한 것이 강제적이지 않아서 힘들다.

그래서 ViewHolder의 개념이 등장하게 된다. 구글의 권장 사항이라 강제적이지는 않다. 다만 위와 같이 inflate와 findViewById를 리스트뷰에서 연속적으로 발생시키면 메모리와 성능에 악영향을 미칠 수 있다. 그래서 ViewHolder 패턴을 사용하는 것을 권장한다.

ListView에 ViewHolder 패턴을 적용한다면?

아래는 ViewHolder 패턴을 적용한 코드이다. convertView == null일 경우에만 inflate와 findViewById가 호출되어 view가 생성된다. 그리고 rootView의 setTag를 호출하여 생성된 ViewHolder를 임시 저장해둔다.

메모리에 문제가 없다면 최초 1회만 생성되고 이후 else 문을 통해서 getTag()를 호출하여 ViewHolder를 꺼내와서 ViewHolder에 접근이 가능한 형태가 만들어지게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// 최초에 convertView가 null이므로, inflate를 처리한다
if (convertView == null) {
// 전역으로 생성한 rootView에 inflate
rootView = inflater.inflate(R.layout.item_list, null);

// ViewHolder을 생성
Holder holder = new Holder();
holder.tv = (TextView) rowView.findViewById(R.id.text);
holder.img = (ImageView) rowView.findViewById(R.id.image);

// setTag : holder 임시 저장
rootView.setTag(holder);
} else {
// rootView에 convertView를 셋팅
rootView = convertView;
// rootView에서 holder을 꺼내온다
holder = (Holder) rootView.getTag();
}

holder.tv.setText(result[position]);
holder.img.setImageResource(imageId[position]);
rowView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Toast.makeText(context, "You Clicked " + result[position], Toast.LENGTH_LONG).show();
}
});
return rootView;
}

위와 같이 구현하는 것이 ListView에 ViewHolder 패턴을 적용하는 것이라고 보면 된다. 하지만 강제적이지 않아서 구현하는 것이 귀찮은 작업이 된다.

그리고 커스텀이 많고 하나의 리스트에 다양한 ViewHolder를 만들기가 쉽지 않다. 예를 들면 다음과 같은 경우이다.

1
2
3
사진이 포함된 ViewHolder
텍스트만 있는 ViewHolder
오른쪽이 스크롤 되는 ListView가 포함된 ViewHolder

물론 우리는 ViewHolder 패턴을 이해하고 만들면 괜찮지만, 대부분의 개발자들은 귀찮을 것을 싫어한다… ㅎㅎ [내 생각^.^] 그래서 구글은 귀찮음을 해소하기 위해 RecyclerView라는 것을 만들어서 내놓았다.

ListView의 장/단점

장점

  • ListView는 간단하게 리스트를 만드는 부분에 있어서는 장점을 가지고 있다. [ex) 텍스트만 있는 리스트]
  • 간단한 아이템 형태를 만드는 경우에는 빠르게 적용이 가능한 ArrayAdapter를 제공한다.

단점

  • 아이템의 애니메이션 처리가 쉽지 않다.
  • 리스트에는 한 개 이상의 View가 필요한 경우가 있지만 커스텀으로 작업하기 쉽지 않다.
  • ViewHolder 패턴을 강제적으로 사용하지 않으므로 고비용의 findViewById가 매번 호출될 수 있다.

RecyclerView

롤리팝(5.0) 버전이 ListView보다 유연하고 성능이 향상된 RecyclerView와 함께 발표되었다. 기존의 ListView는 커스터마이징 하기에 힘들었고, 구조적인 문제로 성능상의 문제도 있어다.

RecyclerView는 ListView의 문제를 해결하기 위해 개발자에게 더 다양한 형태로 커스터마이징 할 수 있도록 제공되었다. RecyclerView와 ListView의 가장 큰 차이점은 LayoutManager와 ViewHolder 패턴의 의무적인 사용, Item에 대한 뷰의 변형이나 애니메이션할 수 있는 개념이 추가된 것이다.

Create Lists

creating Lists and Cards에 정의된 List 표현이다.

widget인 RecyclerView는 LayoutManager를 통해서 View를 그리는 방법을 정의한다. RecyclerView.Adapter에서는 Data의 ViewHolder 정의에 따라서 UI가 선택되고 이를 표현하게 된다.

  • 강제적인 ViewHolder의 적용으로 View의 재사용을 가능하게 해준다.
  • 많은 데이터를 리스트 형태로 제공이 가능하다.
  • RecyclerView.ItemAnimator을 이용하여 Item의 Animator를 이용할 수 있다.
  • LayoutManager를 통해서 아이템의 배치 방법을 다양하게 적용할 수 있다.

주요 클래스

  • Adapter : 기존의 ListView에서 사용하는 Adapter와 같은 개념으로 데이터와 아이템에 대한 View 생성
  • ViewHolder : 재활용 View에 대한 모든 서브 뷰를 보유
  • LayoutManager : 아이템 항목을 어떻게 배치하는가를 결정
  • ItemDecoration : 아이템 항목에서 서브뷰에 대한 처리
  • ItemAnimation : 아이템 항목이 추가, 삭제되거나 정렬될 때 애니메이션 처리를 할 수 있다.
  1. Adapter

리스트뷰는 데이터가 어디서 왔느냐에 따라 BaseAdapter를 상속한 ArrayAdapter(배열로부터 데이터를 가져올 때 사용), CursorAdapter(DB로부터 데이터를 가져올 때 사용), SimpleAdapter(XMl 등으로부터 가져올 때 사용)를 구분하여 사용한다.

하지만 RecyclerView는 Universal한 Adapter를 사용하여 데이터 소스를 처리한다. 이것은 리싸이클러뷰의 유연성을 보여준다. 다음의 3가지 인터페이스를 구현해야 한다.

  • onCreateViewHolder(ViewGroup parent, int viewType) : 뷰 홀더를 생성하고 뷰를 붙여주는 부분이다.
  • onBindViewHolder(CustomViewHolder holder, int position) : 재활용 되는 뷰가 호출하여 실행되는 메소드, 뷰 홀더를 전달하고 어댑터는 position의 데이터를 결합시킨다.
  • getItemCount() : 데이터의 개수 반환

getItemCount() -> onCreateViewHolder() -> onBindViewHolder() 순으로 호출된다.

리스트뷰가 사용했던 getView() 메소드는 매번 호출되면서 null 처리를 해주는 귀찮은 작업을 해줘야했다면, onCreateViewHolder는 새롭게 생성될 때만 호출된다.

  1. ViewHolder

리스트뷰에서는 뷰홀더 패턴을 권장했다. UI를 수정할 때마다 부르는 findViewById()를 뷰홀더 패턴을 이용해 한번만 호출함으로써 리스트뷰의 지연을 초래하는 무거운 연산을 줄여주었다. 이 문제를 리싸이클러뷰에서는 뷰홀더 패턴을 항상 사용하도록(강제하도록) 함으로써 해결했다.

하지만 실제로 앱의 퍼포먼스를 향상시켜주지만 최신의 디바이스는 뷰홀더 패턴을 사용하지 않은 리스트뷰나 리싸이클러뷰의 성능 차이는 미세하다.

단지 차이점은 리싸이클러뷰는 뷰홀더 패턴이 강제되는 것일 뿐이다. 이전의 리스트뷰는 선택적이었지만 성능 차이가 너무 컸기 때문에 변화된 것으로 생각된다. 간과하기 쉬운 중요한 점은 뷰홀더 패턴을 사용한 리스트뷰와 리싸이클러뷰의 성능은 같다.

  1. LayoutManager

리스트뷰는 수직 스크롤만 가능하다. 리스트뷰를 수평으로 사용할 수는 없었다. 그것을 구현하기 위한 몇가지 방법이 있지만 리스트뷰는 그렇게 동작하도록 디자인 되지 않았다고 한다.

그러나 이제 리싸이클러뷰에서는 수직뿐만 아니라 수평 스크롤 또한 지원한다. 뿐만 아니라 더 다양한 타입의 리스트들을 지원하고, 커스텀할 수 있도록 해준다. 많은 타입의 리스트를 사용학 위해서 LayoutManager를 사용하면 된다.

LayoutManager의 종류

  1. LinearLayoutManager
  • Vertical(가로) / Horizontal(세로) 형태로 아이템을 배치한다.
  1. GridLayoutManager
  • 한 줄에 1개 이상의 이미지를 표시할 수 있지만 아이템의 크기는 줄의 첫 번째 아이템의 크기에 따라서 달라질 수 있다.(고정시에는 모두 고정)
  1. StaggeredGridLayoutManager
  • 그리드 형태의 아이템에 크기를 다양하게 적용할 수 있다.
  1. Custom LayoutManager
  • 3개의 레이아웃 매니저를 상속받아 구현할 수 있다.
  1. Item Decoration

리스트뷰에서는 XML에 파라미터를 추가함으로써 쉽게 divide할 수 있었다. 리싸이클러뷰에서는 RecyclerView.ItemDecoration 클래스를 통해 divider를 원하는 아이템에 추가할 수 있도록 되었다. 조금 복잡해졌지만 동적인 데코레이팅이 가능해졌다.

  1. Item Animator

Material Design에 대해 조명된 이후로 리스트에서의 애니메이션을 무궁무진한 가능성을 가지게 되었다. 리스트뷰에서는 아이템의 삽입이나 삭제에 특별한 애니메이션이 없었다.

하지만 리싸이클러뷰에서는 RecyclerView.ItemAnimator 클래스를 통해 애니메이션을 핸들링 할 수 있게 되었다. 이 클래스를 통해서 아이템의 삽입, 삭제, 이동에 대한 커스터마이징이 가능하고, 또한 DefaultItemAnimator가 제공되므로 커스터마이징이 필요 없이 사용할 수도 있다.

notifyItemChanged(int position), notifyItemInserted(int position), notifyItemRemoved(int position)을 ItemAnimator을 통해 특정 아이템에 대한 애니메이션을 발생시킬 수 있습니다.

  1. 클릭 이벤트 처리

터치 이벤트를 통해 사용자가 아이템을 클릭했는지 롱클릭 했는지를 직접처리

RecycleView.OnItemTouchListener은 리싸이클러뷰의 터치 이벤트를 감지한다. 좀 복잡하지만 개발자에게 터치 이벤트를 인터셉트하는 제어권한을 주게 되었다.

안드로이드 공식 문서에서는 터치 이벤트를 인터셉트함으로서 리싸이클러뷰에게 전달하기 전에 조작함으로써 유용하게 사용될 수 있다고 한다.(ListView에서 아이템을 클릭시 콜백 받을 수 있는 리스너는 RecyclerViewd에는 존재하지 않음.)

즉, RecyclerView에는 Click 이벤트에 대한 처리를 자체적으로 할 수 없다. 그래서 onClickListener를 달아줘야 하는 문제가 발생한다.

내가 사용하는 방법으로는 RecyclerView를 사용하는 액티비티에서 View.OnClickListener를 상속받고 그 액티비티의 Context를 RecyclerView.Adapter에서 만든 함수에게 넘긴다.

이 함수는 액티비티에서 받은 Context(여기에 View.OnClickListener이 포함되어 있음)를 Adapter 클래스의 View.OnClickListener 타입의 변수인 ItemClick을 초기화한다.

1
2
3
fun onItemClikc(l : View.OnClickListener){
ItemClick = l
}

그러면 Adapter는 ClickListener 정보를 받아서 onCreateViewHolder에서 만든 View를 리턴하기 전에 그 View에게 onClickListener를 붙여주면 된다.

1
2
3
4
5
6
7
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeFilterViewHolder {
var view = LayoutInflater
.from(parent.context).inflate(R.layout.home_course_filter_list, parent, false)

view.setOnClickListener(onItemClick)
return HomeFilterViewHolder(view)
}
  1. DiffUtil

DiffUtil은 데이터의 변화를 감지하여 뷰를 갱신하는 클래스이다. areItemsTheSame, areContentsTheSame, getChangedPayload 등을 오버라이딩하여 데이터간 변화를 감지한다.

RecyclerView.Adapter에서 notifyItemMoved, notifyItemRangeChanged, notifyItemRangeInserted, notifyItemRangeRemoved 등이 호출되면 DiffUtil에서 해당 범위의 데이터의 변화를 감지하고 뷰를 선택적으로 갱신함으로써 비용이 큰 notifyDataSetChanged 대신 효율적으로 사용될 수 있다.

추가적인 DiffUtil 클래스의 메소드를 확인하고 싶다면 아래 글을 참고하자.
about RecyclerView

정리

그럼 간단하게 ListView와 RecyclerView의 차이점을 표 형식으로 정리해서 보겠다. 아래의 표는 블로그를 참고했으며 참고 링크는 아래에 적어놓았다.

다음 세가지를 비교/정리

  • ListView
    리스트뷰에서는 BaseAdapter를 상속받은 ArrayAdapter나 CursorAdapter 등을 사용한다.
    ViewHolder 패턴을 선택적으로 구현하기 때문에 구현하지 않는 경우 각각의 View를 그릴 때마다 findViewById()를 호출하기 때문에 성능 저하 문제가 발생한다.
    getView() 메소드에서 뷰를 그릴 때마다 findViewById()를 매번 호출하여 성능이 저하된다.

  • ListView+ViewHolder
    리스트뷰에서 ViewHolder 패턴을 구현한다면 성능에 관해서는 RecyclerView와 비슷하지만 기존의 ListView는 뷰 커스텀 작업에 대한 유연성이 떨어진다.

  • RecyclerView
    리싸이클러뷰는 ViewHolder 패턴의 사용을 강제하고 Adapter 클래스를 직접 구현하기 때문에 뷰 커스텀 작업에 대한 유연성이 ListView보다 더욱 쉽고 편하다.

참고