RecyclerView 개념정리 + 사용법

박진성·2023년 5월 18일
0

RecyclerView

RecyclerView 개념정리

RecyclerView란 무엇일까요?

  • AdapterView의 종류중 하나로, 가장 보편적으로 사용된다
  • 같은 형태의 아이템을, 내용만 바꿔 여러번 사용해야될때 사용.
    • ex) 인스타 댓글, 인스타 게시물, 네이버웹툰의 웹툰목록
    • ‘틀’ 은 같지만 내용물이 다른것들!
  • 아이템 개수보다 적은, 화면에 맞는 일정 개수의 ViewHolder만 생성하여, 재활용함!
    • 처음 화면에 보이는 리스트 목록이 딱 10개라면, 위아래 버퍼 생각해서 13 ~ 15개 정도의 View 객체가 생성된다.

RecyclerView와 ListView는 어떤 차이점이 있을까요?

  • 비교
  • ListView의 단점 (View 재활용 없음)
  1. 스크롤시 버벅임. findViewById 메소드 사용으로 비용이 상당히 많이듬
  2. 기본 애니메이션 지원이 없음. 커스터마이징 해야함
  3. 오직 수직 스크롤만 가능

  • 위의 단점들을 보완한것이 RecyclerView 이다

RecyclerView Adapter는 어떤 구성 요소를 가지고 있을까요?

  • ViewHolder
    • 맨처음 생성된, 화면에 맞는 뷰객체를 홀딩(기억)하고있을 객체.

    • 보통 inner class 로 선언

      • 왜? : outerClass 인자에 접근 가능하기 때문에!
    • bind 함수를 따로 선언하는게 편리하다

      inner class ViewHolder(private val binding : RecyclerPostBinding) : RecyclerView.ViewHolder(binding.root){
      
          fun bind(item : PostData){
              binding.ivProfile.setImageResource(item.profileImg)
              binding.tvLikeCount.text = item.likeCount
              binding.tvCommentCount.text = item.commentCount
              ...
      
          }
      }
  • getItemCount
    • 가장 먼저 실행되는 함수

    • View 에 뿌려줄 데이터의 전체길이 리턴하면 됨

      override fun getItemCount(): Int {
          return datas.size
      }
  • onCreateViewHolder
    • 그다음으로 실행되는 함수

    • ViewHolder를 생성하는 함수.

    • View객체 만큼 (처음화면에 보이는 개수 + 2개 정도) 만 호출되고 더이상 호출되지 않음

      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
          val binding = RecyclerPostBinding.inflate(LayoutInflater.from(parent.context),parent,false)
          return ViewHolder(binding)
      }
  • onBindViewHolder
    • 생성된 ViewHolder 에 data를 바인딩 해주는 함수.

    • 데이터가 스크롤 되어서, 맨위 ViewHolder 객체가 맨 아래로 이동한다면, 해당 Layout을 재사용하며 데이터만 바뀐다. 이때마다 onBindViewHolder 가 호출된다

    • 보여질 데이터의 index 값을 position 값으로 알수 있다.

    • 새롭게 올라오는 데이터가 list의 20번째 데이터라면, position으로 20이 들어옴

      override fun onBindViewHolder(holder: ViewHolder, position: Int) {
          holder.bind(datas[position])
      }

RecyclerView Adapter 실행순서 디버깅 해보기

→ 처음 adapter 메소드의 실행순서

  • 화면에 5개만 보이기 때문에, onCreateViewHolder 와 onBindViewHolder가 5번 실행된다

→ 아래 스크롤시 adapter 메소드의 실행순서

position 10부터는 onCreateViewHolder가 호출되지 않는다!!

이는, 화면에서 사라진 position 1, 2 의 View가 재활용 되었다는 뜻.

따라서, ViewHolder는, 화면에 나오는 View + 여분의 View 만 생성시킨채, 더이상 View를 생성하지않고 재활용한다!!

→ 다시 윗스크롤시 adapter 메소드의 실행순서

마찬가지로 더이상 View객체를 생성하지 않는것을 볼 수 있다

RecyclerView를 설정할 때 주의해야 하는 점은 무엇이 있을까요?

  • RecyclerView 의 Adapter는, 전체 아이템 개수에 기반하여, 화면표시 아이템개수 + 여분 아이템 개수 의 ViewHolder를 미리 생성하고, 화면에 표시되지 않는 View를 재사용 하는 구조이다.
  • 따라서, ViewHolder 가 재사용될때, 이전 데이터가 초기화되지 않고 남아있는 문제가 생길수 있기 때문에, 이를 유의해야 한다.

초기 세팅

Step1. 레이아웃 생성

→ item 레이아웃과 recyclerview 자체가 들어갈 layout 두가지 필요

  • RecyclerView 가 있는 레이아웃
<?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=".view.SelectActivity">

		...

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/coinListRV"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="120dp"
        android:layout_marginBottom="80dp"
        app:layout_constraintBottom_toTopOf="@+id/laterTextArea"
        app:layout_constraintTop_toBottomOf="@+id/topTextArea" />

    ...

</androidx.constraintlayout.widget.ConstraintLayout>
  • RecyclerView 의 아이템 하나에 대한 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp">

        ...

    </androidx.cardview.widget.CardView>

</androidx.constraintlayout.widget.ConstraintLayout>

Step2. Adapter 생성

  • Adapter 의 매개변수 : Context, Data 두가지

  • ViewHolder :

  • onCreateViewHolder :

  • onBindViewHolder :

  • getItemCount :

→ view 사용

class SelectRVAdapter(val context : Context, val coinPriceList : List<CurrentPriceResult>)
:RecyclerView.Adapter<SelectRVAdapter.ViewHolder>(){

    inner class ViewHolder(view: View) 
		: RecyclerView.ViewHolder(view){
      
    }

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

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

    override fun getItemCount(): Int {
        return coinPriceList.size
    }

}

→ viewBinding 사용

class SelectRVAdapter(val context : Context, val coinPriceList : List<CurrentPriceResult>)
:RecyclerView.Adapter<SelectRVAdapter.ViewHolder>(){

    inner class ViewHolder(private val viewBinding: IntroCoinItemBinding) 
		: RecyclerView.ViewHolder(viewBinding.root){
      
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
		: ViewHolder {
        val viewBinding = IntroCoinItemBinding
					.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(viewBinding)
    }

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

    override fun getItemCount(): Int {
        return coinPriceList.size
    }

}

Step3. Adapter 에서 각 데이터 삽입

→ onBindViewHolder 에서 삽입(공식홈페이지 내용)

class SelectRVAdapter(val context : Context, val coinPriceList : List<CurrentPriceResult>)
:RecyclerView.Adapter<SelectRVAdapter.ViewHolder>(){

    val likedCoinList = ArrayList<String>()

    inner class ViewHolder(private val viewBinding: IntroCoinItemBinding) 
		: RecyclerView.ViewHolder(viewBinding.root){

				//  레이아웃의 view 를 viewBinding 으로 연결
        val coinName : TextView = viewBinding.coinName

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val viewBinding = IntroCoinItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(viewBinding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
				// 데이터 (coinPriceList) 를 view에 삽입
        holder.coinName.text = coinPriceList[position].coinName
    }

    override fun getItemCount(): Int {
        return coinPriceList.size
    }

}

→ ViewHolder 에서 삽입 (bind 함수 사용. 더 편리함)

class SelectRVAdapter(val context : Context, val coinPriceList : List<CurrentPriceResult>)
:RecyclerView.Adapter<SelectRVAdapter.ViewHolder>(){

    val likedCoinList = ArrayList<String>()

    inner class ViewHolder(private val viewBinding: IntroCoinItemBinding) 
		: RecyclerView.ViewHolder(viewBinding.root){

				//  레이아웃의 view 를 viewBinding 으로 연결
        val coinName : TextView = viewBinding.coinName

				// 데이터 (coinPriceList) 를 view에 삽입
				fun bind(item : CurrentPriceResult){
						coinName.text = item.coinName
				}

				
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val viewBinding = IntroCoinItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(viewBinding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
				holder.bind(coinPriceList[position])
    }

    override fun getItemCount(): Int {
        return coinPriceList.size
    }

}

Step4. recyclerview 와 adapter 연결


val adapter = SelectRVAdapter(context, 데이터)
binding.coinListRV.adapter = adapter
binding.coinListRV.layoutManager = LinearLayoutManager(context)
  • adapter 선언
    • adapter에 context 와 데이터를 매개변수에 넣은뒤 선언
  • Recyclerview 에 adapter를 연결
  • Recyclerview 의 layoutManager 설정

checked 에러 해결

  • 왜 발생하는가?

RecyclerView 의 Adapter는, 전체 아이템 개수에 기반하여,

화면표시 아이템개수 + 여분 아이템 개수 의 ViewHolder를 미리 생성하고,

화면에 표시되지 않는 View를 재사용 하는 구조이다.

따라서, 재사용되는 View에 체크표시가 되어있다면, 그것이 초기화가 안된상태로 새롭게 추가되는 것이다! 따라서, 새롭게 추가되는 View에 대해, 조건을 걸어줘야 해결이 가능하다.

  • 해결방법

별도의 checked list 리스트를 만든 뒤, item을 화면에 띄울때,

checked list에 들어있는 item인지 판별하여 띄운다!!

class MainAdapter(val context : Context, private val datas : List<Int>)
    : RecyclerView.Adapter<MainAdapter.ViewHolder>(){

    // check 표시여부 저장할 데이터사이즈와 동일한 크기의 배열 생성.
    var checkarr : Array<Boolean> = Array(datas.size){false}

    inner class ViewHolder(private val viewBinding: RecyclerItemBinding)
        : RecyclerView.ViewHolder(viewBinding.root){
        fun bind(item : Int){

						// check박스가 표시되야하는지 검사
            viewBinding.check.isChecked = checkarr[position]

            viewBinding.iv.setImageResource(item)

						// check 박스 누르면, 해당 상태 arr 에 저장
            viewBinding.check.setOnClickListener {
                checkarr[position] = viewBinding.check.isChecked
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
            : ViewHolder {
        val viewBinding = RecyclerItemBinding
            .inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(viewBinding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(datas[position])
    }

    override fun getItemCount(): Int {
        return datas.size
    }

}

Item Click 이벤트 처리

Activity나 Fragment 에서 이벤트 처리 시키기

→ Step1. Adapter 에 itemClick interface를 생성한다.

  • position 과 view 를 매개변수로 받는 onClick 함수 선언
  • onBindViewHolder 에, item의 클릭이벤트 처리시 onClick 함수 실행되게끔 이벤트처리
class CoinListRVAdapter(val context : Context, val dataSet : List<InterestCoinEntity>)
    : RecyclerView.Adapter<CoinListRVAdapter.ViewHolder>() {

    interface ItemClick{
        fun onClick(view : View, position: Int)
    }

    var itemClick : ItemClick? = null

   ...

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.itemView.findViewById<ImageView>(R.id.likeBtn).setOnClickListener{ v->
            itemClick?.onClick(v,position)
        }

        ...
    }

    ...
}

→ Step2. Activity 나 Fragment 에서 onClick 함수 구현하기

  • 클릭 이벤트 동작을 viewModel까지 반영 가능
selectedRVAdapter.itemClick = object : CoinListRVAdapter.ItemClick{
    override fun onClick(view: View, position: Int) {

        viewModel.updateInterestCoinData(selectedList[position])

    }
}
profile
Android Developer

0개의 댓글