brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Oct 25. 2020

안드로이드 RecyclerView

Kotlin

RecyclerView?

RecyclerView는 안드로이드 5.0에서 등장한 개념이다. 다음과 같이 여러 개의 데이터를 스크롤이 가능한 목록으로 보여주는 역할을 한다.



그러나 사용자 입장에서는 ListView와 동일한 형태이다. 그런데 왜 RecyclerView가 등장했을까?


RecyclerView를 쓰는 이유

ListView에서는 ViewHolder 패턴을 사용하지 않을 시 성능 저하의 문제가 발생할 수 있다. RecyclerView는 ViewHolder 패턴을 강제적으로 사용하게 하여 성능 저하 문제를 방지한다. 이것이 RecyclerView를 사용하는 여러 가지 이유 중 대표적인 한 가지이다.


그렇다면 또 의문이 생긴다. ViewHolder는 뭐지? 이 궁금증을 해결하기 위해서는 먼저 ListView의 문제점을 명확히 알아야 한다. ListView는 목록의 각 View를 다음과 같은 Adapter를 통해서 inflate 한다.


private class MyAdapter : BaseAdapter() {

    override fun getView(position: Int, convertView: View, container: ViewGroup): View {

        var convertView: View? = convertView

        if (convertView == null) {

            convertView = getLayoutInflater().inflate(R.layout.list_item, container, false)

        }

        convertView.findViewById<TextView>(R.id.text_view).text = 'test'

        return convertView

    }

}


getView()는 목록에 새로운 아이템이 보일 때마다 호출된다. 위 코드는 분기 처리를 통해서 불필요한 View의 inflate를 적절하게 막고 있다. 다시 말하면 View를 재활용하고 있는 것이다. 그러나 여전히 문제점이 있는데 바로 View의 자식인 TextView를 findViewById()를 통해서 가져오고 있다. findViewById()를 자주 호출하는 것은 성능에 영향을 준다. 만약 여러 개의 TextView나 ImageView 등을 가진다면 현재 상태에서는 더욱 성능에 악영향을 미친다.


이러한 문제를 해결하기 위한 것이 바로 ViewHolder 패턴이다. ViewHolder라는 클래스를 생성하여 View의 자식 View까지 포함하여 전체 View를 하나로 관리한다.


class MyAdapter : BaseAdapter() {

    override fun getView(position: Int, convertView: View, parent: ViewGroup): View {

        var convertView: View? = convertView

        val holder: ViewHolder

        if (convertView == null) {

            convertView = getLayoutInflater().inflate(R.layout.list_item, container, false)

            holder = ViewHolder()

            holder.textView = convertView.findViewById<TextView>(R.id.text_view)

            convertView.tag = holder

        } else {

            holder = convertView.tag as ViewHolder

        }

        holder.textView?.text = "test"

        return convertView

    }

    private class ViewHolder {

        var textView: TextView? = null

    }

}


위와 같이 ViewHolder를 사용하면 View를 재활용할 때처럼 findViewById()의 불필요한 호출을 하지 않는다.


RecyclerView 예제

androidx가 등장하기 전에는 지원 라이브러리(supportLibrary-recyclerview-v7)를 통해서 사용했다. 현재는 androidx에서 recyclerview를 지원한다. 따라서 androidx를 활용하여 간단한 예제를 구현해 본다.


0. 전체 코드 


1. build.grade(Module)에 dependecy 추가


dependencies {

...

    implementation 'androidx.recyclerview:recyclerview:1.1.0'

...

}



2. 레이아웃에 RecyclerView 추가

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/mRecyclerView"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </androidx.recyclerview.widget.RecyclerView>

</LinearLayout>



3. itemView 만들기

list_item.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:orientation="horizontal">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="10dp"
            android:src="@mipmap/ic_launcher" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:orientation="vertical">

            <TextView
                android:id="@+id/nameTv"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="20dp"
                android:text="name" />

            <TextView
                android:id="@+id/ageTv"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="15dp"
                android:text="age" />
        </LinearLayout>
    </LinearLayout>

</LinearLayout>



4. item 클래스 생성

RecyclerViewItem.kt


class RecyclerViewItem(var name: String, var age: Int)



5. Adapter 생성

RecyclerViewAdapter.kt


class RecyclerViewAdapter(private val items: ArrayList<RecyclerViewItem>) :
    RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val inflateView =
            LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
        return ViewHolder(inflateView)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = items[position]
        holder.bindViewHolder(item)
    }

    override fun getItemCount() = items.size

    class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
        private var view: View = v
        fun bindViewHolder(item: RecyclerViewItem) {
            view.nameTv.text = item.name
            view.ageTv.text = item.age.toString()
            view.setOnClickListener {
                Toast.makeText(it.context, "Name is ${item.name}", Toast.LENGTH_SHORT).show()
            }
        }
    }
}


RecyclerViewAdapter에서 구현한 주 내용은 다음과 같다.


onCreateViewHolder : ViewHolder를 생성한다.

onBindViewHolder : 데이터를 가져와서 뷰 홀더의 레이아웃을 완성한다.

getItemCount : 아이템의 개수를 알려준다.

ViewHolder : 자식 View를 포함한 레이아웃 단위의 View를 하나의 ViewHolder로 설정한다.


RecyclerView는 Adapter의 onCreateViewHolder()를 통해서 ViewHolder 객체를 요청한다. 그 후 onBindViewHolder()를 호출하여 아이템의 위치와 함께 ViewHolder를 전달한다. 그러면 Adapter는 아이템의 위치 정보를 바탕으로 해당 위치에 맞는 데이터를 View에 설정하여 ViewHolder를 완성한다. (해당 ViewHolder에는 여전히 문제점이 있다. 대표적으로 onClickListener를 반복적으로 등록하고 있다.)



6. MainActivity에서 item 추가하기

MainActivity.kt


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val list = ArrayList<RecyclerViewItem>()
        list.add(RecyclerViewItem("Aaron", 20))
        list.add(RecyclerViewItem("Barbie", 23))
        list.add(RecyclerViewItem("Charles", 17))
        list.add(RecyclerViewItem("Donald", 43))
        list.add(RecyclerViewItem("Edgar", 31))
        list.add(RecyclerViewItem("Felix", 26))
        list.add(RecyclerViewItem("Gilbert", 37))
        list.add(RecyclerViewItem("Ian", 63))
        list.add(RecyclerViewItem("Jade", 53))
        list.add(RecyclerViewItem("Kate", 19))

        val adapter = RecyclerViewAdapter(list)
        mRecyclerView.adapter = adapter
    }
}



RecyclerView는 View를 재활용하는 것이 가장 큰 특징이다. 따라서 필요한 만큼 ViewHolder 객체가 생성되면 onCreateViewHolder()는 더 이상 호출되지 않는다. 실제로 확인해 보려면 onCreateViewHolder()와 onBindViewHolder()에 로그를 추가하고 MainActivity에서 item을 넉넉하게 30개 정도 추가하면 된다.


com.example.kotlinrecyclerview D/TEST: onCreateViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder

com.example.kotlinrecyclerview D/TEST: onCreateViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder

com.example.kotlinrecyclerview D/TEST: onCreateViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder

com.example.kotlinrecyclerview D/TEST: onBindViewHolder


브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari