Do IT SOPT — Android seminar 3

이승우
SOPT
Published in
16 min readDec 6, 2018

3차 세미나는 안드로이드에서 자주 사용되는 FragmentRecyclerview에 관련된 내용을 배웁니다. 이번에는 어렵지만 많은 앱에서 사용되는 기능이기 때문에 꼭 알고 넘어갈 필요가 있습니다. 요즘에 바빠서 글을 작성하지 못해서 얼른 작성해서 업로드 하려고 합니다:)

3차 세미나

  • FragmentStatePagerAdapter
  • RecyclerView

FragmentStatePagerAdapter

간단하게 Fragment가 슬라이드 되는 View를 만들 것인데 그것들을 관리해주는 Adapter라고 생각하면 됩니다. 하지만, FragmentPagerAdapterFragmentStatePagerAdapter가 종종 헷갈리곤 합니다. 그래서 아래에 기본적인 차이점과 특징들을 설명하려고 합니다.

(1). FragmentPagerAdapter

제한된 개수의 항목(즉, Fragment)에 적합합니다. 한 번 생성되면 Fragment의 인스턴스를 FragmentManager에서 절대로 제거하지 않습니다.[Activity가 종료되지 않는한] 현재 보이지 않는 View들은 detach합니다. 그리고 Fragment가 범위 밖으로 나가게 되는 경우, 2주차에서 배웠던 Fragment의 생명 주기 중에서 onDestroyView()가 호출됩니다. 그리고 나중에 다시 이 Fragment로 돌아가게 되면 onCreateView()가 호출될 것입니다.

(2). FragmentStatePagerAdapter

범위를 지정할 수 있으며, 범위 밖의 Fragment 인스턴스를 FragmentManager에서 완전히 제거합니다. 제거된 Fragment의 상태는 FragmentStatePagerAdapter 내부에 저장됩니다. 그리고 초기에 지정한 범위에 다시 들어왔을 때, Fragment 인스턴스를 재생성하고 상태가 복원됩니다. 이는 개수가 미정인 리스트나 항목이 자주 변경되는 리스트에 적합합니다.

  • FragmentPagerAdapter의 메모리 누수

FragmentPagerAdapter의 Fragmenr들은 detached만 되고 Activity가 종료되지 않는 한 절대로 FragmentManager에서 제거되지 않습니다. 이를 사용할 때는 반드시 onDestroyView()에서 현재 View 또는 Context에 대한 참조를 제거해야 합니다. 그렇지 않으면 GC(Garbage Collector)가 전체 View 또는 심지어 Activity를 릴리즈 할 수 없게 만듭니다. 즉 모든 참조에 대한 리스너를 제거해줘야 합니다.

그렇게 하지 않으면 메모리를 고갈시킬 수 있습니다. 예를 들어 FragmentManager에 10개의 Fragment가 있다고 가정하면 이를 모두 스와이프 하면 (OffScreenPageLimit 설정에 따라) 마지막 3개의 View들을 메모리에 유지하는 대신 10개의 View가 유지됩니다. 화면을 회전하면 더욱 안좋은 상황을 초래하는 것이죠. 10개 중 7개는 destroy된 Activity에 참조가 유지됩니다.

  • detache된 좀비 Fragment들

FragmentPagerAdapter에서 Fragment가 FragmentManager에서 제거되지 않는 것에 대해서 잘 생각해봐야 합니다. adapter에 묶인 작은 list의 항목들을 메모리에 수백개의 detach된 Fragment 인스턴스를 가질 수 있습니다. 이렇게 될 경우, 새로운 Fragment 인스턴스를 생성하고 오래된 detache된 Fragment를 유지함으로써 메모리 누수를 유발할 것입니다.

하지만, FragmentStatePagerAdapter에서는 문제가 적습니다. 왜냐하면 앞에서 언급한 것처럼, Fragment 인스턴스를 FragmentManager에서 제거하기 때문이죠.

따라서, FragmentStatePagerAdapter를 사용하면 메모리도 효율적으로 사용할 수 있고, Fragment의 상태도 Adapter 내부에 저장하기 때문에 TabLayout과 사용된다면 이쁜 뷰를 구성할 수 있습니다.

ListView

RecyclerView를 사용하는 것이 저희한테는 편리합니다. 하지만, 이것이 왜 만들어졌는지 무엇이 편리한지 알고 사용해야 의미가 더 있다고 생각이 듭니다. 그래서 ListView에 대해 먼저 알고 나서 RecyclerView를 공부하려고 합니다.

  • ListView는 안드로이드에 임베디드 되어 있는 코드로 동작하며, API level 1 부터 존재했습니다.

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

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

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

위의 코드는 별도의 null 처리가 없으므로, 스크롤을 할 때마다 View의 create가 발생하며 findViewById도 함께 호출됩니다. 리스트뷰의 특성상 하나의 View만 있으면, 연속적으로 사용이 가능한 형태가 만들어지면 되는데, ListView는 강제적이지 않아서 힘듭니다.

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

그럼 ViewHolder 패턴을 적용한다면?

아래는 ViewHolder 패턴입니다. convertView == null일 경우에만 inflate와 findViewById가 호출되어 생성됩니다. 그리고 rootView의 setTag를 호출하여, 생성된 viewHolder를 임시 저장해둡니다.

메모리에 문제가 없다면 최초 1회만 생성되고 이후 else문을 통해서 getTag를 호출하고 이를 통해 viewHolder에 접근이 가능한 형태가 만들어지게 됩니다.

이렇게 구현하는 것이 ViewHolder 패턴이라고 보면 됩니다. 하지만, 강제적이지 않아서 구현하기 귀찮습니다. 그리고 커스텀이 많고, 하나의 리스트에 다양한 ViewHolder를 만들기가 쉽지 않습니다.

예를 들어 다음과 같은 경우입니다.

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

물론, ViewHolder 패턴을 이해하고 만들면 괜찮지만, 우리는 귀찮은 것을 싫어하죠? ㅎㅎ 그래서 구글이 우리의 귀찮음을 해소하기 위해서 Recyclerview라는 것을 만들어 주었습니다.

RecyclerView

위와 같은 불편한 점을 해소하고자 구글이 만든 RecyclerView를 알아봅시다.

안팟장님 자료 참고
  • 위의 사진처럼 반복적인 View를 가진 Item들을 보여주는 즉, 일종의 스크롤 되는 목록(List)를 구현하기 위해서 사용됩니다.
  • 사실, 목록(List) UI가 없는 앱을 찾아보기 힘들 정도로 자주 사용되는 위젯이기 때문에 반드시 알고 넘어가야 할 필요가 있습니다.
  • 이전에 ListView의 장점을 이어받고 단점을 보완한 것이 RecyclerView입니다. 그럼 RecyclerView에 대해 살펴보고 ListView의 어떤 장/단점이 존재하는지 알아보겠습니다.

Create Lists

creating Lists and Cards에 정의된 List 표현입니다

widget인 Recyclerview는 layoutManager를 통해서 View를 그리는 방법을 정의합니다. ReyclerView.Adapter에서 Data의 ViewHolder 정의에 따라서 UI가 선택되고, 이를 표현하게 됩니다.

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

(1). LinearLayoutManager

  • Vertical(가로)/Horizontal(세로) 형태로 아이템을 배치합니다.

(2). GridLayoutManager

  • 한 줄에 1개 이상의 이미지를 표시할 수 있지만, 아이템의 크기는 줄의 첫번째 아이템의 크기에 따라서 달라질 수 있습니다. (고정시에는 모두 고정)

(3). StaggeredGridLayoutManager

  • 그리드 형태의 아이템에 크기를 다양하게 적용할 수 있습니다.

(4). Custom LayoutManager

  • 3개의 레이아웃 매니저를 상속받아 구현할 수 있습니다.

ListView 장/단점

위에서 ListView에 대한 설명을 했지만, 한번 더 짚고 넘어가는 차원에서 장점과 단점만 설명하도록 하겠습니다.

장점

  • ListView는 간단하게 리스트를 만드는 부분에 있어서는 장점이 있습니다.
  • 간단한 아이템 형태를 만드는 경우에는 빠르게 적용이 가능한 ArrayAdapter를 제공합니다.

단점

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

구글에서 추천하는 ViewHolder 패턴을 사용하지 않으면 아래와 같은 코드를 따르게 됩니다.

@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.tv.setText(result[position]);
return rowView;
}

ItemCount에 따라서 매번 getView가 호출되고 이때 위의 코드는 holder부터 inflater.inflate를 초기화하고 findViewById 역시 매번 생성하게 됩니다.

고비용의 findViewById를 매번 하는 것은 성능상 좋지 않고, 메모리의 영향도 받을 수 있습니다. 그래서 다음과 같은 ViewHolder 패턴을 사용할 수 있습니다.

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
rootView = inflater.inflate(R.layout.item_list, null);
Holder holder = new Holder(); // ViewHolder을 생성
holder.tv = (TextView) rowView.findViewById(R.id.text);
rootView.setTag(viewHolder); // setTag
} else {
rootView = convertView;
holder = (Holder) rootView.getTag();
// rootView에서 holder을 꺼내온다
}

holder.tv.setText(result[position]);
return rootView;
}

위와 같은 형태로 구현하게 되는데,매번 구현하기는 귀찮고 서로 다른 ViewHolder를 여러 개 만들어서 사용하기가 쉽지 않습니다. 그래서 RecyclerView가 등장하게 되었지요.

RecyclerView 사용

RecyclerView는 supportLibrary와 안드로이드 5.0(API 21 이상)에서 사용이 가능합니다.

  1. RecyclerView의 정의하기.

다음은 RecyclerView의 XML 정의입니다.

<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

2. LayoutManager 정의하기.

View의 형태를 정의하기 위해서 LayoutManager를 정의해야 합니다. RecyclerView는 기본 정의가 존재하지 않기 때문에 꼭!! 최소 하나씩은 설정해주어야 합니다. 설정하지 않게되면, 화면이 구성되지 않으므로 해매지 말고 초반에 설정해주세요^-^

정의하는 방법은 XML에서 정의하는 방법과 코드상으로 정의하는 방법 두가지가 있습니다.

(1). 먼저, XML 에서 정의하기.

  • app : layoutManager : xml에서 layoutManager를 설정할 수 있습니다.
  • app : spanCount : xml에서 layoutManager에서 사용할 spanCount를 설정할 수 있습니다.

(2). 코드를 통한 정의

  • LinearLayoutManager
mLayoutManger = LinearLayoutManager(this) // vertical일 경우.
mRecyclerView.layoutManager = mLayoutManager
  • GridLayoutManager
mLayoutManger = GridLayoutManager(this,3) // 3은 spanCount
mRecyclerView.layoutManager = mLayoutManager
  • StaggeredGridLayoutManager
mLayoutManger = StaggeredGridLayoutManagermLayoutManger(2, StaggeredGridLayoutManager.VERTICAL)
mRecyclerView.layoutManager = mLayoutManager

3. ViewHolder 정의하기

ViewHolder는 인자로 전달 받은 View에서 RecyclerView의 반복될 아이템들을 findViewById를통해 찾아서 생성합니다.

4. Adapter 정의하기

RecyclerView의 Adapter는 ListView의 ArrayAdapter 처럼 List<Object>를 기본적으로 제공하지 않습니다. 그래서 원하는 형태의 Data 형태도 직접 구현해야 합니다.

5. item 정의하기

RecyclerView는 아이템에 대해서 직접 정의를 해주어야 합니다. 해당 Adapter에서 사용할 아이템을 인자로 받아서 처리하면 됩니다.

class MyAdapter(var items : ArrayList<MyData>) : RecyclerView.Adapter<MyViewHolder>{...// Return the size of your dataset (invoked by the layout manager)
@Override
fun getItemCount() Int = items.length
}

그리고 getItemCount를 기본적으로 상속해야 하며, size를 return 해주면 됩니다.

6. View를 정의

onCreateViewHolder를 기본적으로 상속받고 이를 통해 ViewHolder를 생성하게 됩니다. viewType에 따라 최소 1회만 호출됩니다. viewType이 1개 이상이라면 onCreateViewHolder 역시 1번 이상 호출됩니다.

7. View를 표현하기

onCreateViewHolder를 통해서 View가 생성되면, onBindViewHolder에서 해당 holder의 view에 데이터를 표현하게 됩니다.

RecyclerView는 ViewHolder를 통해서 재사용을 할 수 있도록 설계되어 있으므로 viewType이 한 번 생성된 이후로는 onBindViewHolder만을 호출합니다.

// Replace the contents of a view (invoked by the layout manager)
@Override
fun onBindViewHolder(ViewHolder holder, int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.mTextView.text = mDataset[position]
}

8. viewType 정의하기.

기본적으로 상속을 받는 메소드는 아니지만, viewType을 정의할 수 있습니다. 기본값은 0으로 초기화되지만, 사용에 따라서 getItemViewType을 정의할 수 있습니다.

@Override
fun getItemViewType(position : Int) : Int {
return super.getItemViewType(position)
}

9. RecyclerView에 setAdapter

해당 RecyclerView에 adapter를 정의해주어야 합니다. 방법은 아래와 같이 정의하면 됩니다.

  myRecyclerView.setHasFixedSize(true)

myRecyclerView.layoutManager = LinearLayoutManager(this)

myAdapter = MyAdapter(myDataset)
myRecyclerView.adapter = mAdapter

실습적인 부분보다는 개념적인 부분을 담으려고 노력했습니다. 이 글을 보시는 분들에게 도움이 되기를 바라면서 다음 포스팅을 빠른 시일 내에 올리도록 하겠습니다:)

--

--