Android
Android RecyclerView / 안드로이드 리사이클러뷰 사용법
봉이로그
2023. 12. 4. 20:13
RecyclerView(리사이클러뷰)에 대해 소개를 해보려한다.
리사이클러뷰의 주된 역활은 리스트 페이지를 구성할때, 화면에 노출되는 영역만 렌더링을 하는것이다.
일반적인 리스트뷰를 사용하게 될 시 모든항목들을 렌더링하게되어 성능상의 이슈를 겪을수 있다.
React-Native에서는 FlatList가 비슷한 역활을 한다.
React에서는 react-virtualized 또는 react-window 라이브러리가 비슷한 역활을 제공해줄수 있다.
(직접 만들수도 있다. 오늘의집에서는 react-virtualized의 사용대신에 직접만들었다고 한다.)
https://www.bucketplace.com/post/2020-09-10-%EC%98%A4%EB%8A%98%EC%9D%98%EC%A7%91-%EB%82%B4-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B0%9C%EB%B0%9C%EA%B8%B0/
오늘의집 내 무한 스크롤 개발기 - 오늘의집 블로그
무한 스크롤 적용 시 발생하는 문제점을 오늘의집 개발팀에서는 어떻게 해결했을까?
www.bucketplace.com
본론으로 다시 돌아가보자.
코드로 구현을 해보면서 사용법을 공부해봤다.
우선 build.gradle.kts에 recyclerview 라이브러리 의존성을 추가하자
https://developer.android.com/jetpack/androidx/releases/cardview?hl=ko
Cardview | Jetpack | Android Developers
컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Cardview 둥근 모서리와 그림자로 머티리얼 디자인 카드 패턴을 구현합니다. 최근 업데이트 공개 버전 출시 후
developer.android.com
https://developer.android.com/jetpack/androidx/releases/recyclerview?hl=ko
RecyclerView | Jetpack | Android Developers
컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. RecyclerView 메모리 사용량을 최소화하면서 UI에 많은 양의 데이터를 표시합니다. 이 표에는 androidx.recyclerview 그
developer.android.com
// build.gradle.kts
...
dependencies {
...
implementation("androidx.cardview:cardview:1.0.0") // 나는 카드뷰도 추가했다.
implementation("androidx.recyclerview:recyclerview:1.2.1")
// For control over item selection of both touch and mouse driven selection
implementation("androidx.recyclerview:recyclerview-selection:1.1.0")
...
}
activity_main.xml에 화면 UI 레이아웃을 구성한다.
// 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" // 수직처리
android:padding="20dp"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/my_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
다음 recyclerview에서 사용할 카드 아이템 view를 구성한다.
// item_recyclerview.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_margin="20dp"
app:cardCornerRadius="30dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="20dp">
<ImageView
android:id="@+id/profile_img"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_launcher"
/>
<TextView
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="20dp"
android:text="기본값"
android:textSize="20sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>

Profile클래스를 생성한다.
dataclass로 보통 만든다고하는데, 나는 일반 class로 생성했다.
// Profile.kt
package com.bong.myrecyclerview
import android.util.Log
class Profile(var username: String? = null, var profileImg: String? = null, var id: Int? = null) {
val TAG: String = "로그"
init {
Log.d(TAG, "Profile - () called")
}
}
생성자에 사용할 필드를 선언초기화 했다.
그리고 이제 리사이클러뷰를 사용하기위해서 필요한 어댑터와 홀더 를 구현해줘야 한다.
어댑터의 역활은 리사이클러뷰와 모델을 연결해주는 역활이다.
홀더는 화면에 노출되는 아이템의 영역이라고 할수 있다.
어댑터는 홀더를 통해 데이터를 가져와 화면 레이아웃을 구성한다.
// CustomAdapter.kt
package com.bong.myrecyclerview
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bong.myrecyclerview.databinding.ItemRecyclerviewBinding
import com.bumptech.glide.Glide
class CustomAdapter(val profileList: ArrayList<Profile>) :
RecyclerView.Adapter<CustomAdapter.Holder>() {
val TAG: String = "로그"
// 뷰 홀더가 생성되었을때
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.Holder {
// LayoutInflater를 사용하여 XML 레이아웃 파일과 ViewHolder를 연결
val bindig =
ItemRecyclerviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
// ViewHolder의 생성자에 binding 변수를 전달하여 ViewHolder를 초기화
return Holder(bindig)
}
// onBindViewHolder 함수: 데이터를 ViewHolder에 바인딩하여 화면에 표시 화면에 뷰홀더가 보일때
override fun onBindViewHolder(holder: CustomAdapter.Holder, position: Int) {
holder.name.text = profileList[position].username
Log.d(TAG, "onBindViewHolder: ${profileList[position].id}")
Glide.with(holder.itemView)
.load(profileList[position].profileImg) // 이미지 URL을 로드
.placeholder(R.drawable.loading_spinner) // 이미지 로딩 중에 표시할 placeholder 이미지
.error(R.drawable.error_icon) // 이미지 로드 실패 시에 표시할 이미지
.into(holder.profile)
}
// getItemCount 함수: 데이터 목록의 크기 반환
override fun getItemCount(): Int {
return this.profileList.size
}
inner class Holder(val binding: ItemRecyclerviewBinding) : RecyclerView.ViewHolder(binding.root) {
// ViewHolder 내부의 뷰 요소들을 바인딩
val name = binding.userName
val profile = binding.profileImg
}
}
우선 CustomAdapter 클래스를 생성했다.
CustomAdapter 클래스는 RecyclerView.Adapter<제네릭>를 상속받아 구현한다.
제네릭에는 홀더 클래스를 지정할것인데 홀더는inner class로 선언했다.
홀더 클래스의 생성자에 binding 변수는 item_recyclerview.xml을 선언해주고
RecyclerView의 ViewHolder에 binding.root를 넘겨 ViewHolder를 초기화해준다.
그 후 ViewHolder에서 사용할 내부요소 변수들을 (name, profile) xml로 만든 UI의 @id 와 바인딩 해준다.
이제 holder를 인자로 받는 onBindViewHolder 메소드에서 holder에 담아놓은 name과 profile 변수를 사용할수 있게 된다.
프로필은 ImageView를 사용하여 구성을 하였다.
레퍼런스를 검색해보니 ImageView관련한 라이브러리는 Glide 또는 Picasso 2가지를 대표적으로 언급을 많이했다.
나는 Glide를 사용하기로 했다.
(2가지의 성능을 비교하는 블로그들을 보았지만, 아직 안드로이드 개발을 성능적인 이슈까지 고민하면서 할 필요성은 없을것같아, 그냥 제일 만만한 녀석으로 선택했다. 나중에 앱개발을 주력으로 하게될때면 성능 및 딥한부분까지 고려해봐야할것 같단 생각이듬)
build.gradle에 glide를 추가한다.
// build.gradle.kts
...
dependencies {
implementation("com.github.bumptech.glide:glide:4.12.0") // Glide 라이브러리 의존성
annotationProcessor("com.github.bumptech.glide:compiler:4.12.0")
...
}
CustomAdapter 클래스의 부모인 RecyclerView의 메소드들을 오버라이딩 한다.
onCreateViewHolder |
뷰홀더가 생성되었을때 실행 |
onBindViewHolder |
뷰홀더에 데이터들이 바인딩될때 실행
|
getItemCount |
데이터 목록의 크기를 반환해주는 기능을 수행하지만, 오버라이딩이니 자식클래스에서 어떻게 구현하냐에 따라 다르다. return 타입만 Integer |
MainActivity에서 리스트를 구체화한다.
// MainActivity.kt
package com.bong.myrecyclerview
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import com.bong.myrecyclerview.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
val TAG: String = "로그"
private val dummyList = listOf(
Profile(
"지젤",
"https://blog.kakaocdn.net/dna/bfMmV7/btrlEn2Q7cN/AAAAAAAAAAAAAAAAAAAAAN9INvyByIQlpSq-VC_tt1Gk9hXBVRfBh9lOpbZ-mFA7/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1767193199&allow_ip=&allow_referer=&signature=miYXemMelfemFGq6rh9BrX8ZlGY%3D"
),
Profile(
"윈터",
"https://i.namu.wiki/i/RfoljLTeORMmWEGmjy3qQx_10HIPO9Z4-GdRkx5JpPxpWrvzOiJg78I8Au0ITsUsssY150BXtE8UbeTMX6SlYw.webp"
),
Profile(
"카리나",
"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbbK1X6%2FbtrmLBYKkU8%2FAAAAAAAAAAAAAAAAAAAAAAXYETkqR_Q9K4XRqzExtiisdW_COsHt6fEjhjMwLkPj%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1767193199%26allow_ip%3D%26allow_referer%3D%26signature%3DKOQKOvi9VBj0dag5gXgednvqiDI%253D"
),
Profile(
"닝닝",
"https://i.namu.wiki/i/wr4g6WNmFm_lAqoE3vqJT-tIZ4wQAQe94EoyNKlxLdkPdCjykYQ-fUnBnZGdFu2HfdLlwh7AepucadxQA0qArA.gif"
),
Profile(
"지젤",
"https://blog.kakaocdn.net/dna/bfMmV7/btrlEn2Q7cN/AAAAAAAAAAAAAAAAAAAAAN9INvyByIQlpSq-VC_tt1Gk9hXBVRfBh9lOpbZ-mFA7/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1767193199&allow_ip=&allow_referer=&signature=miYXemMelfemFGq6rh9BrX8ZlGY%3D"
),
Profile(
"윈터",
"https://i.namu.wiki/i/RfoljLTeORMmWEGmjy3qQx_10HIPO9Z4-GdRkx5JpPxpWrvzOiJg78I8Au0ITsUsssY150BXtE8UbeTMX6SlYw.webp"
),
Profile(
"카리나",
"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbbK1X6%2FbtrmLBYKkU8%2FAAAAAAAAAAAAAAAAAAAAAAXYETkqR_Q9K4XRqzExtiisdW_COsHt6fEjhjMwLkPj%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1767193199%26allow_ip%3D%26allow_referer%3D%26signature%3DKOQKOvi9VBj0dag5gXgednvqiDI%253D"
),
Profile(
"닝닝",
"https://i.namu.wiki/i/wr4g6WNmFm_lAqoE3vqJT-tIZ4wQAQe94EoyNKlxLdkPdCjykYQ-fUnBnZGdFu2HfdLlwh7AepucadxQA0qArA.gif"
),
Profile(
"지젤",
"https://blog.kakaocdn.net/dna/bfMmV7/btrlEn2Q7cN/AAAAAAAAAAAAAAAAAAAAAN9INvyByIQlpSq-VC_tt1Gk9hXBVRfBh9lOpbZ-mFA7/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1767193199&allow_ip=&allow_referer=&signature=miYXemMelfemFGq6rh9BrX8ZlGY%3D"
),
Profile(
"윈터",
"https://i.namu.wiki/i/RfoljLTeORMmWEGmjy3qQx_10HIPO9Z4-GdRkx5JpPxpWrvzOiJg78I8Au0ITsUsssY150BXtE8UbeTMX6SlYw.webp"
),
Profile(
"카리나",
"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbbK1X6%2FbtrmLBYKkU8%2FAAAAAAAAAAAAAAAAAAAAAAXYETkqR_Q9K4XRqzExtiisdW_COsHt6fEjhjMwLkPj%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1767193199%26allow_ip%3D%26allow_referer%3D%26signature%3DKOQKOvi9VBj0dag5gXgednvqiDI%253D"
),
Profile(
"닝닝",
"https://i.namu.wiki/i/wr4g6WNmFm_lAqoE3vqJT-tIZ4wQAQe94EoyNKlxLdkPdCjykYQ-fUnBnZGdFu2HfdLlwh7AepucadxQA0qArA.gif"
),
Profile(
"지젤",
"https://blog.kakaocdn.net/dna/bfMmV7/btrlEn2Q7cN/AAAAAAAAAAAAAAAAAAAAAN9INvyByIQlpSq-VC_tt1Gk9hXBVRfBh9lOpbZ-mFA7/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1767193199&allow_ip=&allow_referer=&signature=miYXemMelfemFGq6rh9BrX8ZlGY%3D"
),
Profile(
"윈터",
"https://i.namu.wiki/i/RfoljLTeORMmWEGmjy3qQx_10HIPO9Z4-GdRkx5JpPxpWrvzOiJg78I8Au0ITsUsssY150BXtE8UbeTMX6SlYw.webp"
),
Profile(
"카리나",
"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbbK1X6%2FbtrmLBYKkU8%2FAAAAAAAAAAAAAAAAAAAAAAXYETkqR_Q9K4XRqzExtiisdW_COsHt6fEjhjMwLkPj%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1767193199%26allow_ip%3D%26allow_referer%3D%26signature%3DKOQKOvi9VBj0dag5gXgednvqiDI%253D"
),
Profile(
"닝닝",
"https://i.namu.wiki/i/wr4g6WNmFm_lAqoE3vqJT-tIZ4wQAQe94EoyNKlxLdkPdCjykYQ-fUnBnZGdFu2HfdLlwh7AepucadxQA0qArA.gif"
),
Profile(
"지젤",
"https://blog.kakaocdn.net/dna/bfMmV7/btrlEn2Q7cN/AAAAAAAAAAAAAAAAAAAAAN9INvyByIQlpSq-VC_tt1Gk9hXBVRfBh9lOpbZ-mFA7/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1767193199&allow_ip=&allow_referer=&signature=miYXemMelfemFGq6rh9BrX8ZlGY%3D"
),
Profile(
"윈터",
"https://i.namu.wiki/i/RfoljLTeORMmWEGmjy3qQx_10HIPO9Z4-GdRkx5JpPxpWrvzOiJg78I8Au0ITsUsssY150BXtE8UbeTMX6SlYw.webp"
),
Profile(
"카리나",
"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbbK1X6%2FbtrmLBYKkU8%2FAAAAAAAAAAAAAAAAAAAAAAXYETkqR_Q9K4XRqzExtiisdW_COsHt6fEjhjMwLkPj%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1767193199%26allow_ip%3D%26allow_referer%3D%26signature%3DKOQKOvi9VBj0dag5gXgednvqiDI%253D"
),
Profile(
"닝닝",
"https://i.namu.wiki/i/wr4g6WNmFm_lAqoE3vqJT-tIZ4wQAQe94EoyNKlxLdkPdCjykYQ-fUnBnZGdFu2HfdLlwh7AepucadxQA0qArA.gif"
),
Profile(
"지젤",
"https://blog.kakaocdn.net/dna/bfMmV7/btrlEn2Q7cN/AAAAAAAAAAAAAAAAAAAAAN9INvyByIQlpSq-VC_tt1Gk9hXBVRfBh9lOpbZ-mFA7/img.jpg?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1767193199&allow_ip=&allow_referer=&signature=miYXemMelfemFGq6rh9BrX8ZlGY%3D"
),
Profile(
"윈터",
"https://i.namu.wiki/i/RfoljLTeORMmWEGmjy3qQx_10HIPO9Z4-GdRkx5JpPxpWrvzOiJg78I8Au0ITsUsssY150BXtE8UbeTMX6SlYw.webp"
),
Profile(
"카리나",
"https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdna%2FbbK1X6%2FbtrmLBYKkU8%2FAAAAAAAAAAAAAAAAAAAAAAXYETkqR_Q9K4XRqzExtiisdW_COsHt6fEjhjMwLkPj%2Fimg.jpg%3Fcredential%3DyqXZFxpELC7KVnFOS48ylbz2pIh7yKj8%26expires%3D1767193199%26allow_ip%3D%26allow_referer%3D%26signature%3DKOQKOvi9VBj0dag5gXgednvqiDI%253D"
),
Profile(
"닝닝",
"https://i.namu.wiki/i/wr4g6WNmFm_lAqoE3vqJT-tIZ4wQAQe94EoyNKlxLdkPdCjykYQ-fUnBnZGdFu2HfdLlwh7AepucadxQA0qArA.gif"
),
)
// 데이터를 담을 그릇 즉 배열
var profileList = ArrayList<Profile>()
private val binding by lazy { ActivityMainBinding.inflate((layoutInflater)) }
// 뷰가 화면에 그려질때
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
dummyList.forEachIndexed { index, profile ->
profile.id = index + 1
profileList.add(profile);
}
binding.myRecyclerView.adapter = CustomAdapter(profileList)
binding.myRecyclerView.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
}
}