안녕하세요😃
오랜만에 TIL 이외에 RecyclerView
에 대한 주제로 블로그에 출간하려 합니다.
RecyclerView
는 이름처럼 화면을 스크롤이 될때 뷰를 파괴하지 않고 뷰를 재활용
하여 새로운 아이템을 나타가게 해준다. 이러한 이유로 효율적인 메모리 관리
가 가능하다.
RecyclerView를 사용하기 위해서는 ViewHolder
, Adapter
, LayoutManger
로 구성이 되어 있다.
데이터를 받아서 리사이클러뷰에 표시하는 역할을 합니다.
LinearLayoutManager : 항목을 가로나 세로 방향으로 배치한다.
GridLayoutManager : 항목을 그리드로 배치한다.
StaggeredGridLayoutManager : 항목을 높이가 불규칙한 그리드로 배치한다.
이미지 출처 : https://recipes4dev.tistory.com/154
RecyclerView는 다음과 같은 순서를 가집니다.
1. activity_main에 RecyclerView를 추가합니다.
2. RecyclerView에 표시될 아이템뷰 레이아웃을 추가합니다.
3. 리사이클러뷰에 어댑터를 구현합니다.
4. 어댑터,레이아웃매니저를 지정합니다.
아래와 같이 activity_main에 추가합니다.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
res -> layout에 RecyclerView가 표시될 아이템 뷰를 추가합니다.
저의 경우에는 item_acticle.xml
로 생성하고 아래와 같이 코드를 추가했습니다.
아래와 같이 코드를 추가한 이유는 당근마켓 처럼 ImageView 옆에 TextView가 세로로 배치되는 걸 의도하고 싶어서 저렇게 아래와 같은 코드가 되었습니다.
item_acticle.xml
<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="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="16dp"
android:scaleType="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tittleTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:maxLines="2"
android:textColor="@color/black"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/dateTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/tittleTextView"
app:layout_constraintTop_toBottomOf="@id/tittleTextView" />
<TextView
android:id="@+id/priceTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:textColor="@color/black"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/tittleTextView"
app:layout_constraintTop_toBottomOf="@id/dateTextView" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:background="@color/gray_cc"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
각 항목에 해당하는 뷰 객체를 가지는 ViewHolder는 RecyclerView.ViewHolder를 상속받는다.ViewHolder는 항목 레이아웃 XML 파일에 해당하는 binding 객체만 가지고 있으면 됩니다.
inner class ViewHolder(private val binding: ItemArticleBinding) :
RecyclerView.ViewHolder(binding.root) {
}
Recycler를 위한 Adapter는 RcyclerView.Adapter
와 ListAdapter
를 상속을 받을 수 있는데 차이를 알아보자면
직접 데이터 변경
를 하여 UI를 업데이트를 해야 한다.데이터의 변경을 감지
하며 자동으로 애니메이션 효과와 함께 UI를 업데이트를 하는 기능을 제공한다.정리를 하자면 RcyclerView.Adapter
는 데이터 변경을 수동으로 처리해야 하는 일반적인 Adapter를 만들고 ListAdapter
는 데이터 변경에 대한 감지를 통해 처리할 수 있는 Adapter를 만들 수 있다.
저의 경우에는 DiffUtil를 통해 이전 데이터 세트와 새로운 데이터 세트 간의 차이를 계산하여 변경된 아이템만 업데이트하도록 하여 성능을 개선할 수 있는 장점을 취하기 위해 ListAdapter
를 사용을 하였지만 RcyclerView.Adapter
도 간략하게 정리를 하도록 하겠습니다.
이 예제 코드는 제가 예제로 만든 코드를 가져와 봤습니다.코드를 보면 AdapterViewHolder가 내부 클래스로 정의가 되어 있고 어댑터에 정의해야 하는 함수를 CustomAdapter
이 부분에서 빨간줄이 그어지면서 implement Members를 통해 3개의 함수를 정의하라고 합니다.
class CustomAdapter : RecyclerView.Adapter<CustomAdapter.AdapterViewHolder>() {
inner class AdapterViewHolder(binding: ItemInfoBinding) :
RecyclerView.ViewHolder(binding.root) {
val nameTextView: TextView = binding.nameTextView
val ageTextView: TextView = binding.ageTextView
val korTextView: TextView = binding.korScoreTextView
val deleteButton: Button =binding.Button
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AdapterViewHolder {
val infoBinding =
ItemInfoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return AdapterViewHolder(infoBinding)
}
override fun getItemCount() = studentList.size
override fun onBindViewHolder(holder: AdapterViewHolder, position: Int) {
val item = studentList[position]
holder.nameTextView.text = "이름 : ${item.name} " // 이름은 첫 번째 요소
holder.ageTextView.text = "나이 : ${item.age} " // 나이는 두 번째 요소
holder.korTextView.text = "국어 점수 : ${item.korScore} " // 국어 점수는 세 번째 요소
holder.deleteButton.setOnClickListener {
studentList.removeAt(position)
notifyItemRemoved(position)
notifyItemRangeChanged(position, studentList.size)
}
}
}
이 예제 코드는 아까 당근마켓 처럼 보여주기 위해 item_acticle.xml에 아이템 뷰를 만들었던 예제에요.
여기서는 ListAdapter를 상속받고 DiffUtil을 사용하여 데이터의 변경 사항을 효율적으로 처리하기 위해 DiffUtil을 파라미터로 받았습니다. 여기서도 내부 클래스에 ViewHolder를 넣어 주었고 안에서 item_acticle에 넣어줄 제목,날짜,가격,사진을 처리하였습니다. 여기서 Glide
는 이미지 로딩 라이브러리입니다. 사용한 이유는 나무위키에서 피카츄 사진을 가져오기 위해 사용을 했어요. ListAdapter에도 정의해야 하는 함수가 있는데
ListAdapter는 자동으로 구현해주기 때문에 따로 정의할 필요가 없습니다.
마지막에는 diffUtil을 companion object로 만들어 ListAdapter에서 사용하기 위해 정확히는 ListAdapter의 생성자에 전달해주는 코드를 만들었습니다.
변수 diffUtil은 DiffUtil.ItemCallback의 익명 객체로서 아이템 비교를 담당합니다 ArticleModel
는 데이터 클래스 파일입니다. diffUtil를 implement Members를 해주면 구현해야 하는 2개의 함수가 나타납니다.
areItemsTheSame : 함수는 두 개의 아이템이 동일한지 여부를 확인합니다
areContentsTheSame : 함수는 두 개의 아이템의 내용이 동일한지 여부를 확인합니다.
정리를 하자면 areItemsTheSame
는 아이템을 식별하는 함수이고 areContentsTheSame
는 아이템의 내용 변경 여부를 판단하기 위해 사용한다.
class Adapter : ListAdapter<ArticleModel, Adapter.ViewHolder>(diffUtil) {
inner class ViewHolder(private val binding: ItemArticleBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(articleModel: ArticleModel) {
binding.tittleTextView.text = articleModel.tittle
binding.dateTextView.text = "${articleModel.createtAt}월 01일"
binding.priceTextView.text = articleModel.price
if (articleModel.imageUrl.isNotEmpty()) {
Glide.with(binding.imageView)
.load(articleModel.imageUrl)
.into(binding.imageView)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemArticleBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(currentList[position])
}
companion object {
val diffUtil = object : DiffUtil.ItemCallback<ArticleModel>() {
override fun areItemsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean {
return oldItem.createtAt == newItem.createtAt
}
override fun areContentsTheSame(oldItem: ArticleModel, newItem: ArticleModel): Boolean {
return oldItem == newItem
}
}
}
}
Adapter와 layoutManager를 등록해 화면에 출력합니다.
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
val studentList = mutableListOf<Student>()
lateinit var adapter: CustomAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = CustomAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this@MainActivity)// layoutManager를 이용해 수직으로 배치하는 layoutManager를 설정한다.
adapter.notifyDataSetChanged()//adapter에게 데이터가 변경되었음을 알려 업데이트된 내용을 반영하도록 한다.
이 코드에서는 자신이 만든 Adapte를 변수에 넣고 그 변수에서 submitList() 메서드를 호출하여 데이터 리스트를 업데이트를 하였습니다 내용은 피카츄, 라이츄, 피츄를 나무위키에서 이미지 주소를 가져와 Gilde를 이용해 넣어줬어요. 양식은 dataClass 에 맞게 이름, 날짜(월만 넣어줬어요),가격,이미지Uri을 넣어줬습니다.
fragmentHomeBinding.recyclerView.layoutManager = LinearLayoutManager(context)는 RecyclerView의 레이아웃 매니저를 설정하는 코드입니다.
fragmentHomeBinding.recyclerView.adapter=adapte는 RecyclerView에 어댑터를 설정하는 코드이며 RecyclerView와 어댑터를 연결하여 RecyclerView가 어댑터의 데이터를 표시할 수 있도록 합니다.
private lateinit var adapter : Adapter // Adapter 타입의 adapter 선언
adapter = Adapter() // 자신이 만든 Adapter를 넣어주기
adapter.submitList(mutableListOf<ArticleModel>().apply { // submitList() 메서드를 호출하여 데이터 리스트를 업데이트하는 부분
add(ArticleModel("피카츄",1,"10000원","https://w7.pngwing.com/pngs/441/722/png-transparent-pikachu-thumbnail.png"))
add(ArticleModel("라이츄",2,"15000원","https://static.wikia.nocookie.net/pokemon/images/9/92/%EC%A0%84%EC%A7%84%EC%9D%98_%EB%9D%BC%EC%9D%B4%EC%B8%84.png/revision/latest/scale-to-width-down/1200?cb=20220729043935&path-prefix=ko"))
add(ArticleModel("피카츄",1,"10000원","https://w7.pngwing.com/pngs/441/722/png-transparent-pikachu-thumbnail.png"))
add(ArticleModel("라이츄",2,"15000원","https://static.wikia.nocookie.net/pokemon/images/9/92/%EC%A0%84%EC%A7%84%EC%9D%98_%EB%9D%BC%EC%9D%B4%EC%B8%84.png/revision/latest/scale-to-width-down/1200?cb=20220729043935&path-prefix=ko"))
add(ArticleModel("피츄",3,"22000원","https://i.namu.wiki/i/nOrOqNI0KKLacJSA8Dw_xttWqVR4theEDGtdyIUR2EBveCxx-7q5UkZYF63VWCArP91QgNVoCCPkyLCcUc79YA.webp"))
add(ArticleModel("피카츄",1,"10000원","https://w7.pngwing.com/pngs/441/722/png-transparent-pikachu-thumbnail.png"))
add(ArticleModel("라이츄",2,"15000원","https://static.wikia.nocookie.net/pokemon/images/9/92/%EC%A0%84%EC%A7%84%EC%9D%98_%EB%9D%BC%EC%9D%B4%EC%B8%84.png/revision/latest/scale-to-width-down/1200?cb=20220729043935&path-prefix=ko"))
add(ArticleModel("피츄",3,"22000원","https://i.namu.wiki/i/nOrOqNI0KKLacJSA8Dw_xttWqVR4theEDGtdyIUR2EBveCxx-7q5UkZYF63VWCArP91QgNVoCCPkyLCcUc79YA.webp"))
add(ArticleModel("피카츄",1,"10000원","https://w7.pngwing.com/pngs/441/722/png-transparent-pikachu-thumbnail.png"))
add(ArticleModel("라이츄",2,"15000원","https://static.wikia.nocookie.net/pokemon/images/9/92/%EC%A0%84%EC%A7%84%EC%9D%98_%EB%9D%BC%EC%9D%B4%EC%B8%84.png/revision/latest/scale-to-width-down/1200?cb=20220729043935&path-prefix=ko"))
add(ArticleModel("피츄",3,"22000원","https://i.namu.wiki/i/nOrOqNI0KKLacJSA8Dw_xttWqVR4theEDGtdyIUR2EBveCxx-7q5UkZYF63VWCArP91QgNVoCCPkyLCcUc79YA.webp"))
add(ArticleModel("피츄",3,"22000원","https://i.namu.wiki/i/nOrOqNI0KKLacJSA8Dw_xttWqVR4theEDGtdyIUR2EBveCxx-7q5UkZYF63VWCArP91QgNVoCCPkyLCcUc79YA.webp"))
})
fragmentHomeBinding.recyclerView.layoutManager = LinearLayoutManager(context)
fragmentHomeBinding.recyclerView.adapter=adapter
- https://developer.android.com/guide/topics/ui/layout/recyclerview?hl=ko
- https://recipes4dev.tistory.com/154
- https://gogigood.tistory.com/56
- https://velog.io/@haero_kim/Android-DiffUtil-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0
- https://wooooooak.github.io/android/2019/03/28/recycler_view/
- https://velog.io/@saint6839/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-RecyclerView
- https://velog.io/@appletorch/RecyclerView%EB%9E%80
코드
ListAdapter : https://github.com/kimjinsub1217/AndroidTraining/tree/main/Recyclerview
RcyclerView.Adapter : https://github.com/kimjinsub1217/App-SCHOOL-Unit-2-Kotlin-Example-of-studying-on-Android/tree/main/android35_ex02