[안드로이드 공식문서 파헤치기] Adapter, AdapterView, ListView의 모든 것!

dada·2022년 8월 1일
4
post-thumbnail

참고자료)
Android Developer 도큐먼트 - Adapter
Android Developer 도큐먼트 - AdapterView
Android Developer 도큐먼트 - 앱 위젯
Android Developer 도큐먼트 - ListView
Android Developer 도큐먼트 - BaseAdapter
Android Developer 도큐먼트 - ArrayAdapter
안드로이드 - 리스트뷰 성능 최적화

✅공부배경

  • 공식문서를 보며 ViewPager를 공부하던 중 "PagerAdapter는 AdapterView에 사용되는 어댑터보다 일반적입니다"라는 문장을 봤습니다! AdapterView라는 걸 처음 봤는데 PagerAdapter와 같은 Adapter pattern과 밀접한 관련이 있는 클래스일것이라는 생각이 들어 먼저 공부하고 ViewPager로 넘어가보려 합니다👊

✅AdapterView

  • AdapterView는 ViewGroup를 상속하고 있는 추상 클래스입니다

  • 목록, 그리드 및 스피너와 같은 AdapterView는 보기 계층에서 자식 View 를 유지하지 않기 때문에 일반 레이아웃(예: LinearLayout)과 다릅니다. AdapterViews의 주요 목적은 화면에 대용량 데이터 세트(여러 항목으로 된 리스트같은 모습이라고 생각하시면 됩니다)를 효율적으로 표시하는 것으로, 현재 화면에 보이는 View만 생성하도록 하여 메모리 사용 및 성능을 최적화해야 합니다.

  • 쉽게 말해 AdapterView는 많은 양의 data를 하나의 View로 보여주기 위해 만들어진 추상 클래스입니다. 많은 data를 View로 보여주기 위해 Adapter라는 것을 사용하기 때문에 AdapterView라는 이름으로 불리지 않을까 싶습니다.

  • "여러 항목을 보여주기"위해서는 3가지 구성요소가 필요합니다.
    (1) 데이터(Data)를
    (2) 가공하여 하나의 요소(Item)로 구성하고(Adapter)
    (3) AdapterView에 나열한다

  • AdapterView는 Adapter가 관리하는 데이터를 출력(data set의 형태로 눈에 보이도록)할 수 있게 해주는 View입니다

  • AdapterView의 특징 중 하나는 ScrollView는 모든 내용이 미리 로드되어 있어야 하지만, AdapterView는 화면에 보이는 내용만 로딩할 수 있고, layout을 재사용할 수 있습니다.

  • 재사용이란 layout객체를 다른 객체에 저장시켜둔 뒤 화면에 보여야할 때 다시 꺼내오는 기법을 사용합니다. 이는 Holder를 이용해 구현할 수 있습니다. 또한 layout을 재사용하는 것이 아닌, item view마다 계속 layout을 그린다면 비용이 많이 들기 때문에 AdapterView와 Holder를 이용해 비용을 줄이는 기법을 많이 사용합니다.(Holder를 이용한 AdapterView의 재사용 매커니즘은 아래에 나옵니다!)

    AdapterView는 많은 양의 data를 효율적으로 표시하고 Adapter가 관리하는 데이터를 출력(data set의 형태로 눈에 보이도록)할 수 있게 해주는 View이다. 화면에 보이는 View만 생성하기위해 Holder패턴을 이용해 메모리 사용을 최적화할 수 있다.

✅선택 위젯


그림 출처

  • AdapterView는 '선택위젯'이라고도 할 수 있습니다. 안드로이드에서 여러개의 아이템을 선택할 수 있는 위젯들을 선택 위젯이라고합니다. 다른 위젯들과 다르게 특별히 이름을 붙이는 이유는 사용되는 방식이 기존 위젯과는 조금 다르기 때문입니다. 선택 위젯은 Adapter 패턴을 사용하므로 직접 위젯의 각 아이템에 이미지나 텍스트를넣을 수 없으며 Adapter에서 만들어주는 뷰를 이용해 View 하나의 item을 보여줘야 합니다.

  • 즉, 개발자가 직접 선택위젯의 각 아이템에 데이터를 입력하는게 아니라 Adapter에게 데이터를 설정해야하고 Adpater가 설정된 데이터를 관리합니다.

  • 선택 위젯에 보이는 각 아이템은 화면이 디스플레이 되기 전에 Adapter의 getView()메소드가 호출됩니다. 화면에 보여져야할 아이템이 20개가 있다면 사용자가 화면을 스크롤하면 스크롤 시 getView()도 계속 호출됩니다. 이 메소드는 Adapter에게 가장 중요한 메소드로 리턴하는 뷰가 하나의 item이 됩니다.

✅getView()

  • getView()는 api 레벨1부터 있었던 메소드입니다

  • 각 파라미터에는 position, converView, parent 가 들어갑니다

  • position : 아이템의 인덱스를 의미하는 것으로 ListView에서 보일 아이템의 위치 정보입니다. 0부터 시작해서 아이템 개수만큼 파라미터로 전달됩니다

  • converView : getView()로 전달되는 뷰는 현재 인덱스에 해당하는 뷰 객체인데 안드로이드에서는 선택위젯(AdapterView)이 데이터가 많아 스크롤 될 때 뷰를 재활용하는 매커니즘을 가지고 있어, 한번 만들어진 View가 화면 상에 그대로 다시 보일 수 있도록 하고 있습니다. 따라서 이 View가 null이 아니라면(객체가 메모리에 있다면) 이를 재활용 할 수 있는 것입니다. 예를 들어 리스트가 100 개의 아이템을 가지고 있는데 화면에서 보여줄 수 있는 아이템의 개수가 4개면 이 4개의 아이템이 먼저 보여진 후 스크롤 될 때마다 이미 만들어진 4개의 뷰들을 그대로 사용하면서 데이터만 바꾸어 보여주는 방식입니다

  • parent : 이 뷰를 포함하고 있는 부모 컨테이너 객체입니다

✅Adapter

  • Adapter는 인터페이스이고, 데이터와 AdapterView(ListView, GridView 등)을 연결하는 하나의 다리 역할을 하는 객체입니다. 일종의 데이터(Array, List, DB, Provider 등)을 받아 관리하고, AdapterView로 출력할 수 있는 형태로 만들어 주는 것이 Adapter입니다.

  • 즉 Adapter는 data를 받아서 AdapterView에 출력할 수 있도록 데이터를 저장 가공하며 data item에 접근할 수 있게 해주는 중간다리입니다.

  • 따라서 Adapter가 알고있어야 하는건 아래와 같습니다
    • 저장, 가공할 data를 알고 있어야 한다
    • 어떤 형태로 item을 구성할지에 대한 layout을 알고있어야 한다(RecyclerView에선 LayoutManage가 담당)
    • data와 layout을 mapping할 수 있어야 한다
  • Adapter의 종류에는 ArrayAdapter(List), CurosrAdapter(DB), SimpleAdapter(다른 것들에 비해 확장성 낮음)이 있습니다
  • 안드로이드는 Adapter의 서브클래스를 위 그림처럼 여러개 제공하고 있습니다. 각 하위 단계의 class들은 상위 단계의 class들을 상속하고 있습니다 그리고 가장 상위에 인터페이스인 Adapter를 구현하고 있습니다

    Adapter는 data를 받아서 AdapterView에 출력할 수 있도록 데이터를 저장 가공하며 data item에 접근할 수 있게 해주는 중간다리다.

🟧ListView

  • AdapterView 중에서 가장 많이 사용되는 View는 ListView입니다. ListView는 RecyclerView를 이해하는 기본 개념이 되기도 하기 때문에 본 포스팅에선 ListView에 대해 자세히 다루겠습니다

  • ListView는 수직 스크롤 가능한 뷰 컬렉션을 표시합니다.

  • ListView는 AdapterView이기 때문에 Adapter을 사용해야하며 ArrayAdapter, BaseAdapter를 통해 구현할 수 있습니다

✔BaseAdapter로 ListView 구현하기

1. xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

2.mainActivity


data class CarForList(
    val car: String,
    val engine: String
)

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //리스트뷰에 얹고 싶은 리스트 만들기
        val carList = ArrayList<CarForList>()
        for (i in 0 until 10) {
            carList.add(CarForList("$i 번째 자동차", "$i 순위 엔진"))
        }

        val listView = findViewById<ListView>(R.id.listView)
        val adapter = ListViewAdapter(carList, this)
        listView.adapter = adapter

    }

    //BaseAdapter을 상속받아 Adapter 구현
    //사용자의 데이터를 받아 뷰(View)를 생성해주는 객체로 ListView와는 독립적으로 동작하는 객체입니다.
    //ListView는 Adpater로부터 생성된 뷰(View)를 받아 ListView의 한 항목으로 배치
    class ListViewAdapter(
        val carForList: ArrayList<CarForList>,
        val context: Context
    ) : BaseAdapter() {
        override fun getCount(): Int {
            return carForList.size
            //adapter가 아이템의 개수를 알아야 화면에 그릴 아이템 개수 조절 가능
            //그리고자 하는 아이템 리스트에서의 아이템 전체갯수
        }

        override fun getItem(p0: Int): Any {
            return carForList.get(p0)
            //p0은 listView의 순서임 해당 인덱스에 해당하는 아이템 정보 알려줌
            //그리고자 하는 아이템 리스트의 하나(포지션에 해당하는는
        }

        override fun getItemId(p0: Int): Long {
            return p0.toLong()
            //아이디를 포지션으로 해주겠다=포지션은 listView의 아이템의 인덱스
            //해당 포지션에 위치한 아이템 뷰의 아이디 설정
        }

        override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
           // getView( )가 리턴하는 객체가 하나의 텍스트 뷰나, 버튼 같은 하나의 뷰가 아니라 레이아웃일 수도 있다
           //여기서는 layout을 View로 return하고 싶기 때문에 수동으로 inflate해준다
            Log.d("ㅡㅡㅡㅡㅡㅡㅡgetViewㅡㅡㅡㅡㅡㅡㅡ", "getView호출됩니다~")

            val layoutInflater = LayoutInflater.from(context)
            var view = layoutInflater.inflate(R.layout.activity_second, null)

            //인플레이션 후 메모리에 올라갔으니 찾는다
            var carNameTextView = view.findViewById<TextView>(R.id.car_name)
            var carEngineTextView = view.findViewById<TextView>(R.id.car_engine)

            //찾은 view들의 text에 운하는 data를 bind해준다
            carNameTextView.text = carForList[p0].car  //p0은 positon임
            carEngineTextView.text = carForList[p0].engine

            //bind한 view객체를 return한다
            return view
        }
    }

  • 화면에 보이는 item의 개수는 5개이지만 화면이 스크롤 될 때마다 getView()가 호출되어 총 item 개수인 10개보다 많은 수의 getView()가 호출됨을 알 수 있습니다

  • 하지만 이처럼 getView()가 계속 호출되면, 똑같은 view를 계속 inflate하고 똑같은 id를 계속 findViewById로 찾기 때문에 매우 비효율적입니다. ListView의 특성 상 하나의 View만 있으면 이 View안에 들어가는 data만 바꾸면 되기 때문입니다.

  • 또한 아이템에 이미지가 있거나 많은 데이터가 있으면 스크롤 할 때마다 View를 계속해서 만들기 때문에 성능상에 큰 문제를 야기하며 매끄러운 스크롤을 보장하지 못합니다.

🟧ListView 성능개선1- View재활용: inflate 횟수 줄이기

  • 앞선 문제 중 View를 매번 생성하는 문제를 해결하기 위해 구글은 눈속임 기법을 사용합니다. 바로 사용자의 눈에 보이는 View만 생성해두고 스크롤이 발생하면 맨 위에 있던 View를 가장 아래로 가져오고 그 안에 들어간 data만 바꾸는 방법이었습니다.

  • 예를 들어 배열이 100개이고 화면에 보여지는 View들이 5개면 Adapter에 100개의 Object들이 Setting 된 후 모든 Object들의 100개의 View를 생성하여 보여주는 것이 아니라 5개의 Object만 보여줍니다. 여기서 ListView 스크롤이 내려가게되면 첫 번째 View는 사라지고 6번째의 View가 보여집니다. 이 때 6번째의 View도 새로 생성되는 것이 앙니라 기존의 View를 재사용하여 값만 새로이 Setting하는 개념입니다

  • 실제 화면에 그려지는 아이템을 convertView라는 배열로 관리합니다. 화면에 보여지는 만큼 convertView를 생성하고 스크롤하면 이 View를 재활용합니다. 즉 convertView는 ListView의 재활용 View입니다

  • convertView는 Adapter의 getView()의 두번째 파라미터임을 위에서 설명했었습니다. ListView는 화면에 새로운 아이템을 표시할 때마다 Adapter의 getView()를 무조건 호출하게 되는데 앞선 getView 코드블럭 안의 예시에서는 이 convertView가 null인지 아닌지 체크하는 코드 없이(null이 아니면 재사용할 View가 있다는 것입니다) 작성했기 때문에 getView()에서 View를 매번 inflate했습니다

  • 이를 토대로, converView가 null인지 아닌지 체크해서 재사용할 ConvertView를 가져옵니다

  • 화면에 보이는 View의 개수만큼만 inflate가 실행되고 converView가 재사용됩니다

🟧ListView 성능개선2- ViewHolder사용하기: findViewById() 줄이기

  • 하지만 getView가 호출될때마다 매번 findViewById가 호출되고 있습니다. view는 재활용하는데, 정작 데이터 셋팅할 때 convertView의 자식뷰(textView 등) 정보를 매번 findViewById()를 통해 다시 가져와 재연결하기 때문에 비효율적입니다. 문제는 ViewHolder패턴을 이용하면 해결가능합니다
 override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View {
            // getView( )가 리턴하는 객체가 하나의 텍스트 뷰나, 버튼 같은 하나의 뷰가 아니라 레이아웃일 수도 있다
            //여기서는 layout을 View로 return하고 싶기 때문에 수동으로 inflate해준다
            val rootView: View
            val holder: ViewHolder

            if (convertView == null) {
                Log.d("ㅡㅡㅡㅡㅡㅡㅡgetViewㅡㅡㅡㅡㅡㅡㅡ", "inflate합니다~")
                val layoutInflater = LayoutInflater.from(context)
                holder = ViewHolder()
                rootView = layoutInflater.inflate(R.layout.activity_second, null)

                //id를 holder에 담아둔다
                holder.carName = rootView.findViewById<TextView>(R.id.car_name)
                holder.carEngine = rootView.findViewById<TextView>(R.id.car_engine)

                rootView.tag = holder
            } else {
                //View의 tag를 이용해서 holder를 지정한다
                holder = convertView.tag as ViewHolder
                rootView = convertView
            }

            //찾은 view들의 text에 원하는 data를 bind해준다
            holder.carName?.text = carForList[pos].car
            holder.carEngine?.text = carForList[pos].engine

            //bind한 view객체를 return한다
            return rootView
        }
    }

    class ViewHolder {
        var carName: TextView? = null
        var carEngine: TextView? = null
    }
  • ViewHolder는 각 뷰를 보관하는 Holder 객체로 이야기 할 수 있습니다. ListView는 inflate를 최소화 하기 위해서 뷰를 재활용 하는데, 이 때 각 뷰의 내용을 업데이트 하기 위해 findViewById 를 매번 호출 해야합니다. 이로 인해 성능저하가 일어남에 따라 ItemView의 각 요소를 바로 엑세스 할 수 있도록 저장해두고 사용하기 위한 객체입니다." 정도로 설명할 수 있을 것 같습니다.

  •   개별 View가 존재하는 경우 View의 setTag / getTag를 이용하여  findViewById를 하지 않는 ViewHolder Pattern 방법입니다.

  • view의 Tag를 이용하여 각 viewholder를 한번씩 생성한 후 view의 tag에 홀더를 저장할 경우, listView의 뷰가 재활용 될 때(convertView), viewHolder의 정보도 함께 불러올 수 있음으로 findViewById를 해서 자식뷰를 불러올 필요가 사라집니다

    • findViewById() : findViewById()는 ViewGroup 밑에 있는 모든 뷰들을 전부 한 번씩 순회하며 id값을 비교하는 과정을 거치기 때문에 자원이 많이 듭니다
  • rootView의 setTag를 호출해서 생성된 ViewHolder에 임시 저장을 해둡니다.
    최초 1회만 생성되고 이후 else문을 통해서 getTag를 호출해 ViewHolder를 꺼내와서 ViewHolder에 접근이 가능한 형태가 만들어지는 것입니다.

  • converview의 tag에 viewHolder 넣어주면, convertView가 null이 아닐때 viewholder 정보도 가져오면서, 성능 향상이 가능하기 때문에 좀 더 매끄러운 스크롤이 가능합니다

  • 즉 Holder에 id(data)를 담아두고 사용하는 것입니다. 해당 방법은 Google I/O에서 권장되는 방법입니다

🟧RecyclerView로의 이전

  • 따라서 listView의 경우 convertView와 ViewHolder를 이용해 무거운 작업을 최소화시키고, layout을 재사용하고 불필요한 findViewById를 줄여 성능을 최적화 할 수 있지만 ViewHodler패턴이 강제되지 않으며 하나의 리스트에 다양한 ViewHolder를 만들기도 어렵다는 문제가 있습니다(어떤건 사진이 있는 ViewHolder 등)

  • 또한 ListView 자체에 애니메이션 처리 문제, 수직스크롤 밖에 안된다는 점 등의 문제가 대두되며 구글은 ListView보다 유연하고 성능이 좋은 RecyclerView라는 것을 내놓았습니다

  • RecyclerView는 ListView에서 강제되지 않았던 ViewHolder패턴을 강제하고 수직 스크롤 밖에 되지 않았던 문제를 해결하며 item의 '배치'만을 담당하는 LayoutManager라는 개념을 추가했습니다. 이후 내용은 안드로이드 공식문서 파헤치기 시리즈 RecyclerView 포스팅과 이어집니다.

ListView의 장/단점
장점

  • ListView는 간단하게 리스트를 만드는 부분에 있어서는 장점을 가지고 있다. [ex) 텍스트만 있는 리스트]
  • 간단한 아이템 형태를 만드는 경우에는 빠르게 적용이 가능한 ArrayAdapter를 제공한다.
    단점
  • 아이템의 애니메이션 처리가 쉽지 않다.
  • 리스트에는 한 개 이상의 View가 필요한 경우가 있지만 커스텀으로 작업하기 쉽지 않다.
  • ViewHolder 패턴을 강제적으로 사용하지 않으므로 고비용의 findViewById가 매번 호출될 수 있다.
profile
'왜?'라는 물음을 해결하며 마지막 개념까지 공부합니다✍

0개의 댓글