많은 데이터를 한번에 보여주려고 하면 메모리 등 자원 문제로 버벅이거나 심하면 OOM(Out of Memory)이 발생할 수 있습니다.
RecyclerView
는 이런 성능문제를 해결하고 데이터를 효율적으로 보여주는 View입니다.
RecyclerView는 예전부터 공개된 라이브러리이고 지금까지 많이 쓰여왔습니다. 예제들이 많이 있지만, 여기서는 코틀린과 AndroidX로 구현하는 방법을 알아보겠습니다.
프로젝트 생성
Kotlin
과 Use AndroidX artifacts
를 선택해주세요.(Android Studio 버전이 3.4 미만이라면 Use AndroidX artifacts
옵션이 없습니다. 마이그레이션을 하거나 코드를 변경해줘야 합니다)
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
에 타이틀을 추가해주세요.
<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만 제공하면 되기 때문에 추가로 구현할 것은 없습니다.
class YoutubeItem(val image: Drawable, val title: String) {
}
RecyclerView 구현
먼저 activity_main.xml
파일에 기존에 있던 코드를 삭제하고 LinearLayout안에 RecyclerView
를 추가해주세요.
<?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
을 생성하고 아래처럼 구현을 해주세요.
<?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
는 다음과 같습니다.
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를 생성하여 화면에 보여줍니다.
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
}
}
이제 앱을 실행해보세요. 데이터인 유튜브 썸네일과 타이틀들이 순차적으로 출력이 됩니다.
구분선(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))
...
다시 앱을 실행해서 결과를 확인하면 출력되는 데이터 사이에 구분선이 추가된 것을 볼 수 있습니다.
물결(Ripple) 효과
지금 데이터 리스트를 터치하면 토스트가 발생하도록 리스너를 추가했었습니다. 토스트가 보이니 클릭이 된 것을 알 수 있는 데 조금 더 시각적으로 Ripple effect가 보였으면 좋겠습니다.
list_item.xml
의 ConstraintLayout에 background
속성을 추가하고 ?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">
....
실행해서 클릭해보면 물결 효과가 발생합니다.
LayoutManager 변경
지금까지 RecyclerView에 데이터를 출력하고, 구분선과 물결 효과를 넣었습니다.
이번에는 LayoutManager
를 변경해보고 어떻게 출력되는지 확인해보겠습니다.
앞에서 LinearLayoutManager
로 설정했었는데요, GridLayoutManager
로 변경해보겠습니다.
xml 코드만 변경해주면 됩니다. activity_main.xml
의 RecyclerView에서 layoutManager
속성을 GridLayoutManager
로 설정해주고
spanCount
속성을 3으로 설정해주세요.
수정된 코드는 다음과 같습니다.
<?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열로 데이터가 출력됩니다.
정리
RecyclerView를 구현하려면 Adapter, Divider, LayoutManager를 구현해야 합니다. 개발자가 자신의 앱에 유연하게 적용할 수 있도록 역할이 분리되어 설계가 되었습니다. 기본적으로 자주 사용되는 객체들은 SDK에서 제공을 하고 있고, 필요한 경우 자신만의 객체를 생성할 수 있습니다.
참고
- 샘플 코드는 GitHub에 있습니다
- RecyclerView - Android developer
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력