HOME > android > basic

안드로이드 - RecyclerView 구현 방법 (AndroidX, Kotlin)

JSFollow23 Nov 2018

많은 데이터를 한번에 보여주려고 하면 메모리 등 자원 문제로 버벅이거나 심하면 OOM(Out of Memory)이 발생할 수 있습니다. RecyclerView는 이런 성능문제를 해결하고 데이터를 효율적으로 보여주는 View입니다.

RecyclerView는 예전부터 공개된 라이브러리이고 지금까지 많이 쓰여왔습니다. 예제들이 많이 있지만, 여기서는 코틀린과 AndroidX로 구현하는 방법을 알아보겠습니다.

프로젝트 생성

Empty Activity로 생성합니다. android studio empty project

KotlinUse AndroidX artifacts를 선택해주세요.(Android Studio 버전이 3.4 미만이라면 Use AndroidX artifacts옵션이 없습니다. 마이그레이션을 하거나 코드를 변경해줘야 합니다) android studio androidx artifact

AndroidX로 프로젝트를 생성하지 않았다면, 메뉴에서 [Refactor] -> [Migrate to AndroidX...] 를 누르시면 AndroidX를 사용하는 프로젝트로 마이그레이션이 됩니다.

RecyclerView를 사용하려면 앱 gradle에서 의존성을 추가해야 합니다.

dependencies {
  ....
  implementation "androidx.recyclerview:recyclerview:1.0.0"
}

데이터 Wrapper

우리가 만들 샘플은 Youtube의 썸네일과 타이틀을 연속적으로 보여주는 앱입니다. 리사이클러뷰를 만들기 전에 썸네일과 타이틀을 관리하는 데이터 객체를 만들고 시작하는 것이 좋겠습니다.

완성된 샘플 코드는 GitHub에 있습니다. 여기서 /res/drawable/에서 image로 시작하는 파일들을 다운받아 모두 자신의 drawable 폴더로 복사해주세요. 그리고 strings.xml에 타이틀을 추가해주세요.

/res/values/strings.xml
<resources>
    <string name="app_name">Sample</string>

    <string name="title01">10 Best Practices for Moving to a Single Activity</string>
    <string name="title02">Cost of a Pixel Color (Android Dev Summit 18)</string>
    <string name="title03">Foldables, App Bundles and more from Android Dev Summit 18!</string>
    <string name="title04">Fun with LiveData (Android Dev Summit 18)</string>
    <string name="title05">Keynote (Android Dev Summit 18)</string>
    <string name="title06">Modern WebView Best Practices (Android Dev Summit 18)</string>
    <string name="title07">Performance Analysis Using Systrace (Android Dev Summit 18)</string>
    <string name="title08">Preferential Practices for Preferences (Android Dev Summit 18)</string>
    <string name="title09">That’s a wrap on Android Dev Summit 2018!</string>
    <string name="title10">Vitals: Past, Present and Future (Android Dev Summit 18)</string>
</resources>

파일과 문자열은 모두 추가했습니다. 이제 이 두개의 데이터를 관리하는 YoutubeItem 클래스를 만들어주세요. 데이터를 저장하고 getter/setter만 제공하면 되기 때문에 추가로 구현할 것은 없습니다.

YoutubeItem.kt
class YoutubeItem(val image: Drawable, val title: String) {

}

RecyclerView 구현

먼저 activity_main.xml 파일에 기존에 있던 코드를 삭제하고 LinearLayout안에 RecyclerView를 추가해주세요.

/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            tools:listitem="@layout/list_item"/>

</LinearLayout>

LayoutManager 속성은 View를 어떤 배열로 보여줄지 결정하는 객체입니다. 저희는 LinearLayoutManager로 설정하였습니다.

listitem 속성은 디자인 미리보기 기능을 적용할 때 어떤 레이아웃을 이용하여 보여줄지 설정하는 것입니다. list_item.xml을 생성하고 아래처럼 구현을 해주세요.

/res/layout/list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="15dp"
    android:paddingBottom="15dp">

    <ImageView
        android:id="@+id/thumbnail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:adjustViewBounds="true"/>

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        app:layout_constraintTop_toBottomOf="@+id/thumbnail"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

우리는 RecylcerView를 이용하여 YoutubeItem 객체를 순차적으로 출력되도록 할 것인데요. YoutubeItem의 데이터가 보여질 레이아웃 파일을 list_item.xml로 정의하였습니다. RecylcerView는 이 View들을 순차적으로 출력하게 됩니다.

이제 xml은 모두 생성하였고 코틀린 코드만 추가하면 됩니다. 우리가 보여줄 데이터는 RecyclerView에 직접 넣지 않고 Adapter에 추가됩니다. RecyclerView는 Adapter를 통해 데이터를 얻고 View를 생성합니다. 그래서 Adapter 객체도 생성해야 합니다.

RecyclerAdapter에서 구현해야 할 함수들은 onCreateViewHolder, onBindViewHolder, getItemCount, ViewHolder정도 입니다.

  • getItemCount: 보여줄 아이템 개수가 몇개인지 알려줍니다
  • onCreateViewHolder: 보여줄 아이템 개수만큼 View를 생성합니다
  • onBindViewHolder: 생성된 View에 보여줄 데이터를 설정(set)해줍니다
  • ViewHolder: ViewHolder 단위 객체로 View의 데이터를 설정합니다
class RecyclerAdapter(private val items: ArrayList<YoutubeItem>) :
    RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) {
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
            RecyclerAdapter.ViewHolder {
    }

    class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
        fun bind(listener: View.OnClickListener, item: YoutubeItem) {
        }
    }
}

RecyclerView가 초기화 될 때 onCreateViewHolder가 호출됩니다. 여기서 이전에 구현한 list_item.xml를 View로 생성합니다.

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

View가 생성되면 onBindViewHolder가 호출됩니다. 썸네일과 타이틀을 View에 설정하고, 클릭하면 토스트가 발생하는 Listener도 구현하였습니다.

override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) {
    val item = items[position]
    val listener = View.OnClickListener {it ->
        Toast.makeText(it.context, "Clicked: ${item.title}", Toast.LENGTH_SHORT).show()
    }
    holder.apply {
        bind(listener, item)
        itemView.tag = item
    }
}

class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
    private var view: View = v
    fun bind(listener: View.OnClickListener, item: YoutubeItem) {
        view.thumbnail.setImageDrawable(item.image)
        view.title.text = item.title
        view.setOnClickListener(listener)
    }
}

완성된 RecyclerAdapter.kt는 다음과 같습니다.

RecyclerAdapter.kt
class RecyclerAdapter(private val items: ArrayList<YoutubeItem>) :
    RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: RecyclerAdapter.ViewHolder, position: Int) {
        val item = items[position]
        val listener = View.OnClickListener {it ->
            Toast.makeText(it.context, "Clicked: ${item.title}", Toast.LENGTH_SHORT).show()
        }
        holder.apply {
            bind(listener, item)
            itemView.tag = item
        }
    }

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

    class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {

        private var view: View = v

        fun bind(listener: View.OnClickListener, item: YoutubeItem) {
            view.thumbnail.setImageDrawable(item.image)
            view.title.text = item.title
            view.setOnClickListener(listener)
        }
    }
}

이제 MainActivity.kt에서 RecyclerView와 Adapter를 설정하고 데이터를 추가하면 됩니다. ArrayList에 YoutubeItem 객체들을 만들고 추가하였습니다.

ArrayList를 인자로 RecyclerAdapter를 생성하였고, RecyclerView에 Adapter를 설정하였습니다. 이제 RecyclerView는 Adapter를 통해 데이터를 가져오고 그 데이터를 기반으로 View를 생성하여 화면에 보여줍니다.

MainActivity.kt
class MainActivity : AppCompatActivity() {

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

        val list = ArrayList<YoutubeItem>()
        list.add(YoutubeItem(getDrawable(R.drawable.image01)!!, getString(R.string.title01)))
        list.add(YoutubeItem(getDrawable(R.drawable.image02)!!, getString(R.string.title02)))
        list.add(YoutubeItem(getDrawable(R.drawable.image03)!!, getString(R.string.title03)))
        list.add(YoutubeItem(getDrawable(R.drawable.image04)!!, getString(R.string.title04)))
        list.add(YoutubeItem(getDrawable(R.drawable.image05)!!, getString(R.string.title05)))
        list.add(YoutubeItem(getDrawable(R.drawable.image06)!!, getString(R.string.title06)))
        list.add(YoutubeItem(getDrawable(R.drawable.image07)!!, getString(R.string.title07)))
        list.add(YoutubeItem(getDrawable(R.drawable.image08)!!, getString(R.string.title08)))
        list.add(YoutubeItem(getDrawable(R.drawable.image09)!!, getString(R.string.title09)))
        list.add(YoutubeItem(getDrawable(R.drawable.image10)!!, getString(R.string.title10)))

        val adapter = RecyclerAdapter(list)
        recyclerView.adapter = adapter
    }
}

이제 앱을 실행해보세요. 데이터인 유튜브 썸네일과 타이틀들이 순차적으로 출력이 됩니다.

android recyclerview

구분선(Divider) 넣기

각각의 유튜브 데이터 사이에 구분선을 넣고 싶을 수 있습니다. RecyclerView에 Divider를 추가하면 구분선이 출력됩니다. MainActivity.kotlin의 recyclerView 코드 아래에 addItemDecoration로 Divider 객체를 추가해주세요.

....
list.add(YoutubeItem(getDrawable(R.drawable.image09)!!, getString(R.string.title09)))
list.add(YoutubeItem(getDrawable(R.drawable.image10)!!, getString(R.string.title10)))

val adapter = RecyclerAdapter(list)
recyclerView.adapter = adapter

recyclerView.addItemDecoration(
    DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
...

android recyclerview

다시 앱을 실행해서 결과를 확인하면 출력되는 데이터 사이에 구분선이 추가된 것을 볼 수 있습니다.

물결(Ripple) 효과

지금 데이터 리스트를 터치하면 토스트가 발생하도록 리스너를 추가했었습니다. 토스트가 보이니 클릭이 된 것을 알 수 있는 데 조금 더 시각적으로 Ripple effect가 보였으면 좋겠습니다.

list_item.xmlConstraintLayoutbackground 속성을 추가하고 ?android:attr/selectableItemBackground을 입력합니다. View를 클릭했을 때 배경의 효과를 설정하는 내용입니다. 리소스는 SDK에서 기본적으로 제공하는 것을 사용하였습니다. 필요에 따라서 직접 만들 수 있습니다.

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="15dp"
    android:paddingBottom="15dp"
    android:background="?android:attr/selectableItemBackground">

    ....

실행해서 클릭해보면 물결 효과가 발생합니다.

android recyclerview

LayoutManager 변경

지금까지 RecyclerView에 데이터를 출력하고, 구분선과 물결 효과를 넣었습니다. 이번에는 LayoutManager를 변경해보고 어떻게 출력되는지 확인해보겠습니다.

앞에서 LinearLayoutManager로 설정했었는데요, GridLayoutManager로 변경해보겠습니다. xml 코드만 변경해주면 됩니다. activity_main.xml의 RecyclerView에서 layoutManager속성을 GridLayoutManager로 설정해주고 spanCount속성을 3으로 설정해주세요.

수정된 코드는 다음과 같습니다.

/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
            app:spanCount="3"
            tools:listitem="@layout/list_item"/>

</LinearLayout>

spanCount는 Grid의 열을 몇개로 설정할지 입니다. 3으로 설정하면 3열로 데이터가 출력됩니다.

android recyclerview

정리

RecyclerView를 구현하려면 Adapter, Divider, LayoutManager를 구현해야 합니다. 개발자가 자신의 앱에 유연하게 적용할 수 있도록 역할이 분리되어 설계가 되었습니다. 기본적으로 자주 사용되는 객체들은 SDK에서 제공을 하고 있고, 필요한 경우 자신만의 객체를 생성할 수 있습니다.

참고