리사이클러 뷰
RecyclerView
RecyclerView는 안드로이드 앱에서 리스트 형태의 데이터를 표시하는데 사용되는 위젯.
여러 아이템을 스크롤 가능한 리스트로 표현하며, 많은 아이템을 효율적으로 관리하고 보여주는 역할을 함
- Recycle
- View를 재활용
- ListView와의 차이점
- LIstView
- 스크롤 할 때마다 위에 있던 아이템 삭제, 맨 아래 아이템은 생성 이 과정을 반복
- 아이템 개수 만큼 삭제, 생성 반복 성능에 좋지 않다
- RecyclerView
- 스크롤 할 때마다 위에 있던 아이템이 재활용 되며 아래로 이동하여 재사용 됨
- ex) 10개 정도 View를 만들고 10개를 재활용하여 사용함
- View를 계속해서 만드는 ListView의 단점 보완함
RecyclerView 기본 구조
- Adapter
- 데이터를 목록 형태로 보여주기 위해 사용됨
- 데이터를 아이템 뷰와 연결하는 역할을 함
- 데이터와 RecyclerView 사이의 통신을 위한 연결체
- ViewHolder
- 아이템 뷰를 저장하고 표시하는 역할을 함
- 스크롤 해서 위로 올라간 View를 재활용하기 위해 View를 기억하는 역할을 함
- LayoutManager
- 데이터나 아이템들이 RecyclerView 내부에서 배치되는 형태를 관리함
- LinearLayoutManager : 수평, 수직으로 배치 시켜줌
- GridLayoutManager : 그리드 화면으로 배치 시켜줌
- StaggeredGridLayoutManager : 높이가 불규칙한 그리드 화면으로 배치 시켜줌
- 데이터나 아이템들이 RecyclerView 내부에서 배치되는 형태를 관리함
LayourManager 정렬 방식
- LinearLayoutManager : 수직(기본값)
kotlinbinding.recyclerView.layoutManager = LinearLayoutManager(this)
- LinearLayoutManager : 수평
true
: 순서 /false
: 역순
kotlinbinding.recycleView.layoutManager = LinearLayourManger(this, LinearLayoutManager.HORIZONTAL, false)
- GridLayoutManager : 수직(기본값)
kotlinbinding.recycelrView.layoutManager = GridLayoutManager(this,3)
- GridLayoutManager : 수평
kotlinbinding.recycelrView.layoutManager = GridLayoutManager(this,3,GridLayoutManager.HORIZONTAL,false)
- StaggeredGridLayoutManager
VERTICAL
: 수직 /HORIZONTAL
: 수평- 각 항목의 크기 별로 불규칙한 그리드 형식
kotlinbinding.recycelrView.layoutManager = StaggeredGridLayoutManager(3,LinearLayoutManager.VERTICAL)
View Binding
이름처럼, 레이아웃과 코틀린 파일을 묶어주는 방식임
기존의 findViewById를 대체 하며 상호 작용하는 코드를 쉽게 작성할 수 있음
예를 들어 `TextView` 를 업데이트 하는 경우
kotlinimport android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// findViewById로 View를 가져오기
val textView: TextView = findViewById(R.id.textView)
textView.text = "Hello, findViewById!"
}
}
⇒ 딱 봐도 코드가 길어보이고 관리하기 어려워 보인다…
다음 ViewBinding
kotlinimport android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.app.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ViewBinding 초기화
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// ViewBinding으로 View를 참조
binding.textView.text = "Hello, ViewBinding!"
}
}
⇒ 코드가 간결해보이고 가독성이 좋다!
- findViewById와의 차이점
-
Null Safety
바인딩 기능을 사용하면, 앱이 레이아웃의 각 뷰를 직접 참조할 수 있게 해주는 안전한 코드를 자동으로 생성함. 이는 뷰를 사용할 때 ‘null’값으로 인한 오류 즉, 아직 뷰가 렌더링 되지 않았는데 그 뷰를 사용하려고 할 때 생길 수 있는 문제를 예방 해줌
ex) 레이아웃에 버튼이 있어야 하는데 아직 버튼이 생성되지 않았다면, 바인딩은 이를 안전하게 처리하여 앱이 크러쉬 되지 않게 해줌
또, 만약 레이아웃의 일부만 뷰가 있다면, 뷰 바인딩은 해당 뷰가 ‘가능성 있는 null(Nullable)’임을 알려줘, 개발자가 더 주의 깊게 코드를 작성할 수 있도록 도와줌
-
타입 안정성(Type Safety)
XML 레이아웃 파일에서 정의된 뷰의 타입과 자동 생성된 바인딩 클래스의 필드 타입이 항상 일치하기 때문에, 타입이 서로 맞지 않아 발생할 수 있는 오류를 방지해준다.
ex) 이미지 뷰(ImageView)에 텍스트를 설정하려고 하면 오류가 발생하는데, 바인딩을 사용하면 이런 실수를 할 가능성이 없어짐
즉, ImageView ⇒ ImageView, TextView ⇒ TextView 로만 사용되게끔 하며, 잘못된 타입 사용으로 인한 오류 발생 방
-
RecyclerView 사용
1. 의존성 추가: build.gradle
파일에 RecyclerView 의존성 주입
kotlinimplementation("androidx.recyclerview:recyclerview:1.3.1")
gradle 설정
xmlandroid{ viewBindint{ enabled = true } buildFeatures{ viewBinding = true } }
2. 아이템 레이아웃 생성 : 아이템 하나의 레이아웃을 작성 함
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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/black"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
3. Adapter 생성 : RecyclerView.Adapter 클래스를 상속한 Adapter클래스 생성
4. ViewHolder 생성 : RecyclerView.ViewHolder 클래스를 상속한 ViewHolder 클래스를 생성함
kotlinpackage com.example.constraintlayout_practice.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.constraintlayout_practice.databinding.ItemBinding
// RecyclerView의 Adapter 클래스. String 리스트를 받아서 RecyclerView의 각 항목에 데이터를 표시함
class RecyclerViewAdapter(private val items: List<String>) : RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
// ViewHolder를 생성하는 메서드
// 여기서 레이아웃 XML 파일을 바인딩하고 ViewHolder에 전달
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// item.xml 레이아웃을 바인딩
val binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding) // ViewHolder를 반환
}
// 각 ViewHolder에 데이터를 바인딩하는 메서드
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position]) // position에 해당하는 데이터를 ViewHolder에 바인딩
}
// RecyclerView의 전체 항목 수 반환
override fun getItemCount(): Int = items.size
// RecyclerView의 ViewHolder 클래스. 하나의 항목에 해당하는 View를 관리
inner class ViewHolder(private val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) {
// 데이터(item)를 View에 바인딩하는 메서드
fun bind(item: String) {
binding.textView.text = item // item 데이터를 TextView에 설정
}
}
}
5. LayoutManager 설정 : RecyclerView에 사용할 레이아웃 매니저를 설정함
kotlinpackage com.example.constraintlayout_practice.ui
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.constraintlayout_practice.R
import com.example.constraintlayout_practice.adapter.RecyclerViewAdapter
import com.example.constraintlayout_practice.custom.CustomItemDecoration
import com.example.constraintlayout_practice.databinding.ActivityRecyclerviewBinding
class RecyclerViewActivity : AppCompatActivity() {
private lateinit var binding : ActivityRecyclerviewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRecyclerviewBinding.inflate(layoutInflater)
setContentView(binding.root)
val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
val adapter = RecyclerViewAdapter(items)
val backButton = binding.goBack
val dividerDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
val spaceInPixels = resources.getDimensionPixelSize(R.dimen.recycler_item_space)
val spaceDecoration = CustomItemDecoration(spaceInPixels)
binding.recyclerView.addItemDecoration(dividerDecoration)
binding.recyclerView.addItemDecoration(spaceDecoration)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
backButton.setOnClickListener() {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
}
}
결과

image.png
다음처럼, 밑으로 내리면 item들을 순회하면서 item을 보여줌
추가 아이템 간격 및, 구분선 추가 하기
- DivderItemDecoration 클래스를 사용, 수평선 추가
kotlinval dividerDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
binding.recyclerView.addItemDecoration(dividerDecoration)
-
커스텀으로 간격 설정 후 추가
- 간격 설정
xml<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="recycler_item_space">16dp</dimen> <dimen name="recycler_margin">24dp</dimen> </resources>
- ItemDecoration을 상속받는 커스텀 클래스 생성
kotlinpackage com.example.constraintlayout_practice.custom import android.graphics.Rect import android.view.View import androidx.recyclerview.widget.RecyclerView class CustomItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() { override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { super.getItemOffsets(outRect, view, parent, state) outRect.bottom = space } }
- 레이아웃에 간격 설정
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=".ui.RecyclerViewActivity"> <Button android:id="@+id/goBack" android:layout_width="300dp" android:layout_height="200dp" android:layout_margin="100dp" android:paddingStart="@dimen/recycler_margin" android:paddingEnd="@dimen/recycler_margin" android:clipToPadding="false" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" android:text="뒤로 가기" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/guideline3" app:layout_constraintVertical_bias="1.0" tools:listitem="@layout/item" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.2"/> </androidx.constraintlayout.widget.ConstraintLayout>
- 액티비티에 속성 추가
kotlinpackage com.example.constraintlayout_practice.ui import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.example.constraintlayout_practice.R import com.example.constraintlayout_practice.adapter.RecyclerViewAdapter import com.example.constraintlayout_practice.custom.CustomItemDecoration import com.example.constraintlayout_practice.databinding.ActivityRecyclerviewBinding class RecyclerViewActivity : AppCompatActivity() { private lateinit var binding : ActivityRecyclerviewBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityRecyclerviewBinding.inflate(layoutInflater) setContentView(binding.root) val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5") val adapter = RecyclerViewAdapter(items) val backButton = binding.goBack val dividerDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) val spaceInPixels = resources.getDimensionPixelSize(R.dimen.recycler_item_space) val spaceDecoration = CustomItemDecoration(spaceInPixels) binding.recyclerView.addItemDecoration(dividerDecoration) binding.recyclerView.addItemDecoration(spaceDecoration) binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.adapter = adapter backButton.setOnClickListener() { val intent = Intent(this, SecondActivity::class.java) startActivity(intent) } } }
결과

image.png
클릭 시 이벤트 처리
대체로 RecyclerView에서 아이템을 클릭할 때 이벤트를 처리하는 방법은 ViewHolder
에서 클릭 리스너를 설정하는 방식으로 처리하는 경우가 많음
단계
- ViewHolder에서 클릭 리스너 설정: 각 아이템을 클릭했을 때 발생할 이벤트를 처리 할 수 있도록
ViewHolder
내에서 클릭 리스너를 설정함
kotlinprivate val onItemClick: (String) -> Unit
inner class ViewHolder(private val binding: ItemBinding) : RecyclerView.ViewHolder(binding.root) {
// 아이템 클릭 이벤트 리스너 설정
init {
itemView.setOnClickListener {
val clickedItem = items[bindingAdapterPosition]
onItemClick(clickedItem)
}
}
- RecyclerViewActivity에서 아이템 클릭 시 클릭한 아이템 RecyclerView로 만들기
kotlin// 추가된 아이템을 담을 리스트 생성
private val addedItems = mutableListOf<String>()
// RecyclerView 어댑터 생성, 아이템 클릭 시 addedItems 리스트에 아이템 추가
val adapter = RecyclerViewAdapter(items) { item ->
addedItems.add(item) // 클릭된 아이템 추가
Toast.makeText(this, "$item added", Toast.LENGTH_SHORT).show()
// addedItemsRecyclerView 업데이트
updateAddedItemsRecyclerView()
}
// 추가된 아이템을 표시할 두 번째 RecyclerView 설정
binding.addedItemsRecyclerView.layoutManager = LinearLayoutManager(this)
val addedItemsAdapter = RecyclerViewAdapter(addedItems) { addedItem ->
// 추가된 아이템 클릭 시 토스트 메시지 표시
Toast.makeText(this, "$addedItem Clicked", Toast.LENGTH_SHORT).show()
}
binding.addedItemsRecyclerView.adapter = addedItemsAdapter
// addedItemsRecyclerView 업데이트 함수
private fun updateAddedItemsRecyclerView() {
// 두 번째 RecyclerView 어댑터에 데이터 변경을 반영
val addedItemsAdapter = binding.addedItemsRecyclerView.adapter as RecyclerViewAdapter
addedItemsAdapter.notifyDataSetChanged()
}
심화 (Youtube api 활용 동영상 재생 기능 (1/4))
Muti-View 타입 활용해서 썸네일, 영상 제목, 생성 일자, 서브 타이틀 가져오기
-
RecyclerView 아이템 레이아웃 파일
다양한 데이터를 한 화면에서 보여주기 위해서 Multi-View 방식 사용, 이를 위해 각 아이템의 레이아웃 정의 함
Activitiy 파일
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=".ui.RecyclerViewActivity"> <!-- 뒤로 가기 버튼 --> <Button android:id="@+id/goBack" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="뒤로 가기" android:layout_marginTop="16dp" android:layout_marginStart="16dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" /> <!-- 첫 번째 RecyclerView (영상 리스트) --> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="0dp" android:layout_height="300dp" app:layout_constraintTop_toBottomOf="@id/goBack" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> <!-- 가이드라인 --> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.2"/> </androidx.constraintlayout.widget.ConstraintLayout>
item 파일
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"> <ImageView android:id="@+id/thumbnail" android:layout_width="100dp" android:layout_height="70dp" android:layout_margin="10dp" android:src="@drawable/combined_image" android:contentDescription="Thumbnail" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/title" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Video Title" android:textSize="16sp" android:layout_marginTop="10dp" app:layout_constraintStart_toEndOf="@+id/thumbnail" app:layout_constraintTop_toTopOf="parent"/> <TextView android:id="@+id/subtitle" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Subtitle" android:textSize="14sp" app:layout_constraintStart_toStartOf="@+id/title" app:layout_constraintTop_toBottomOf="@+id/title" /> <TextView android:id="@+id/date" android:layout_width="0dp" android:layout_height="wrap_content" android:text="2025-01-19" android:textSize="12sp" android:layout_marginTop="5dp" app:layout_constraintStart_toStartOf="@id/title" app:layout_constraintTop_toBottomOf="@id/subtitle" /> </androidx.constraintlayout.widget.ConstraintLayout>
-
RecyclerViewAdapter 설정(더미 데이터, Multi-view 활용)
kotlinpackage com.example.constraintlayout_practice.adapter import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.example.constraintlayout_practice.data.VideoItem import com.example.constraintlayout_practice.databinding.ItemVideoBinding // class VideoAdapter(private val videoList: List<VideoItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val binding = ItemVideoBinding.inflate(LayoutInflater.from(parent.context), parent, false) return VideoViewHolder(binding) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val videoItem = videoList[position] (holder as VideoViewHolder).bind(videoItem) } override fun getItemCount(): Int = videoList.size inner class VideoViewHolder(private val binding: ItemVideoBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(item: VideoItem) { binding.title.text = item.title binding.subtitle.text = item.subtitle binding.date.text = item.date // 썸네일을 리소스 이미지로 설정 binding.thumbnail.setImageResource(item.thumbnailRes) } } }
-
모델 생성
kotlinpackage com.example.constraintlayout_practice.data data class VideoItem ( val title : String, val subtitle : String, val date : String, val thumbnailRes: Int, // val thumbnail : String, // val videoUrl : String )
-
Activity 및, LayoutManager 코드 작성
kotlinclass RecyclerViewActivity : AppCompatActivity() { private lateinit var binding : ActivityRecyclerviewBinding private val videoList = mutableListOf<VideoItem>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityRecyclerviewBinding.inflate(layoutInflater) setContentView(binding.root) videoList.add(VideoItem("Video 1", "Subtitle 1", "2025-01-19", R.drawable.combined_image)) videoList.add(VideoItem("Video 2", "Subtitle 2", "2025-01-19", R.drawable.combined_image)) videoList.add(VideoItem("Video 3", "Subtitle 3", "2025-01-19", R.drawable.combined_image)) videoList.add(VideoItem("Video 3", "Subtitle 3", "2025-01-19", R.drawable.combined_image)) val adapter = VideoAdapter(videoList) // val adapter = VideoAdapter(videoList) { videoItem -> // // 썸네일 클릭 시 해당 영상 URL로 이동 // val fragment = VideoFragment.newInstance(videoItem.videoUrl) // supportFragmentManager.beginTransaction() // .replace(R.id.fragmentContainer, fragment) // .addToBackStack(null) // .commit() // } val spaceInPixels = resources.getDimensionPixelSize(R.dimen.recycler_item_space) val spaceDecoration = CustomItemDecoration(spaceInPixels) binding.recyclerView.layoutManager = LinearLayoutManager(this) binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration(spaceDecoration) } }
결과

image.png

image.png

image.png
Recycle View로 썸네일, 타이틀, 생성일을 받아올 수 있게 됐다
이제 Recycle View로 뼈대 만들었으니, Fragment 학습 후 실제 데이터 연동 해봐야 겠다
끗!