본문 바로가기

Android

RecyclerView : 리사이클러

728x90

RecyclerView에 관한 포스팅을 해보겠습니다. 리사이클러뷰에 대한 포스팅 전에 ListView와 GridView에 대한 간단한 설명도 포함했습니다.

 

ListView와 GridView에 대해서도 간단히 알아보면, 리스트뷰는 1차원 형태, 그리드뷰는 2차원 형태로 데이터를 나열할 수 있습니다. 예를들자면 리스트뷰는 전화번호부, 문자 목록 등이 있고, 그리드뷰는 사진첩과 같은 형태라고 이해하면 됩니다.

RecyclerView

리사이클러뷰는 새로운 ViewGroup의 한 종류로, ListView와 GridView 기능을 제공합니다. 

 

왜 리사이클러뷰를 사용하는 이유가 무엇일까?

리스트뷰는 간단하게 목록을 만들 수 있다는 장점이 있지만 커스텀으로 작업하기가 어렵고 아이템들을 생성할 때마다 ViewBinding을 반복해서 성능저하가 일어나며, 아이템의 갯수를 모두 준비하게 되어 사용자의 화면에 표시되지 않는 아이템들도 미리 준비를 하게되는데, 이로인해 계속해서 뷰를 inflate 시키면, Out of Memory가 발생하게 됩니다.

 

반면, 리사이클러뷰는 ViewHolder 패턴을 강제로 구현하게하여 한번의 ViewBinding을 하고 아이템을 생성할 때 바인딩된 뷰 객체를 재활용하고 다양한 방향의 레이아웃 구성이 가능하고 아이템 애니메이션을 적용할 수 있습니다. 

하지만 리스트뷰에는 아이템 클릭 리스너가 인터페이스로 존재하지만, 리사이클러뷰는 onClickListener로 직접 구현해야 합니다.

 

WorkFlow

번호 세부 설명
1 RecyclerView가 표시될 위치를 결정
2 RecyclerView의 Item들이 배치될 형태 지정
3 ItemView의 Layout을 생성
4 Adapter 구현
5 ViewHolder 구현

 

RecyclerView 주요 클래스

①. Adapter : ListView에서 사용하는 Adapter와 같은 개념이며, 표시될 View를 생성합니다. Data List로 부터 아이템 뷰를 만드는 역할을 담당합니다. 

getItemCount() : 가장 먼저 실행되는 함수로, 보여줄 목록, 즉 데이터의 개수를 반환합니다.

 

onCreateViewHolder(parent: ViewGroup, viewType: Int) : getItemCount() 함수 다음으로 호출되는 함수로, 뷰 홀더를 생성합니다. 전체 리스트 목록 중 화면에 표시되는 아이템이 10개라면, 위 아래 버퍼를 생각하여 약 15개 정도의 뷰 객체가 생성됩니다. 즉, 뷰 객체를 담고있는 ViewHolder가 생성되는데, 15번 정도의 호출이 이루어지게 됩니다.

 

onBindViewHolder(holder: WeatherHourItemViewHolder, position: Int) : 재활용되는 뷰 홀더 객체가 호출해서 실행되며, 뷰 홀더를 전달하고 어댑터는 Data List에 position의 데이터를 바인딩합니다.

 

②. LayoutManager : 아이템뷰들을 Linear, Grid 등의 방식으로 리사이클러뷰 내부에 배치되도록 지정할 수 있습니다.

 

③. ViewHolder : View를 담는 그릇을 의미하며, 재활용을 위해 View에 대한 서브 뷰와 View 객체를 기억하는 것이 ViewHolder 입니다. 즉, 스크롤을 위 아래로 이동할 때 ViewHolder가 가지는 서브 뷰에 데이터만 바꿔주어 View 객체는 그대로이면서 데이터만 바뀌게 됩니다.

 

④. ItemDecoration : 아이템 항목에서 서브뷰에 대한 처리, ItemAnimation : 아이템 삽입, 삭제 등에 대한 커스텀이 가능하고, notifyItemChanged(), notifyItemInsrted() 등을 ItemAnimator를 통해 특정 아이템에 대한 애니메이션을 발생시킬 수 있습니다.

 

 

/** WeatherFragment **/

viewModel_weather.apply {
    weatherHourDataLiveData.observe(requireActivity(), Observer {
        if (!it.isEmpty()) {
            weatherHourGridRecyclerViewSetting(it)
        }
    })
}

private fun weatherHourGridRecyclerViewSetting(weatherHourList: ArrayList<WeatherHourData>){
        weatherhourGridRecyclerViewAdapter = WeatherHourGridRecyclerViewAdapter()
        weatherhourGridRecyclerViewAdapter.updateItems(weatherHourList)

        binding.weatherHourRecyclerView.layoutManager =
            GridLayoutManager(App.instance, 12, GridLayoutManager.VERTICAL, false)
        // context, 가로 item 수, 출력 방향, item의 첫, 끝 중 시작 위치
        binding.weatherHourRecyclerView.adapter = weatherhourGridRecyclerViewAdapter
    }
/** Adapter **/

class WeatherHourGridRecyclerViewAdapter : RecyclerView.Adapter<WeatherHourItemViewHolder>() {

    private var weatherHourList = ArrayList<WeatherHourData>()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeatherHourItemViewHolder {
        return WeatherHourItemViewHolder(LayoutInflater.from(parent.context)
            .inflate(R.layout.layout_weatherhour_item, parent, false))

        //LayoutInflater.from() : LI에 static으로 정의된 LI.from()을 통해 LI를 만드는 방법
        //내부적으로 getSystemService를 호출하고 있으며, 같은 context에서는 같은 객체를 리턴하기
        // 때문에 굳이 멤버 변수로 선언해 놓지 않고 필요할 때 마다 호출해서 사용해도 괜찮다.

        //inflate() : resource: View를 만들고 싶은 레이아웃 파일의 id
        //root: 생성될 View의 parent를 명시해줍니다. null일 경우 LayoutParams 값을
        // 설정할 수 없어 XML 내의 최상위 android:layout_xxxxx 값들이 무시되어 merge tag를 사용할 수 없다
        // attachToRoot : true로 설정하면 root의 자식 View로 자동으로 추가됨, 이때 root는 null일 수 없다
        // return : attachToRoot에 따라서 리턴값이 달라집니다. true일 경우 root, false일 경우 xml 내 최상위 뷰가 리턴
    }
    // 뷰가 묶였을 때 데이터를 뷰홀더에 넘겨준다.
    override fun onBindViewHolder(holder: WeatherHourItemViewHolder, position: Int) {
        holder.bindWithView(weatherHourList[position])
    }
    // 보여줄 목록의 갯수
    override fun getItemCount(): Int {
        return weatherHourList.size
    }
    //외부에서 어답터에 arraylist을 넘겨준다
    fun updateItems(weatherHourList : ArrayList<WeatherHourData>){
        Log.d(Constants.TAG, "${this::class.simpleName} updateItems() called")
        this.weatherHourList = weatherHourList
        notifyDataSetChanged()
    }
}
/** ViewHolder **/

class WeatherHourItemViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView) {

    private var binding : LayoutWeatherhourItemBinding = LayoutWeatherhourItemBinding.bind(itemView.rootView)

    private val hour_date = binding.tvHourDate
    private val hour_icon = binding.imageHourIcon
    private val hour_text = binding.tvHourText
    private val hour_temp = binding.tvHourTemp
    private val hour_chance = binding.tvHourChanceOfRain

    fun bindWithView(weatherItem: WeatherHourData){
        Log.d(Constants.TAG, "${this::class.simpleName} bindWithView() called")

        hour_date.text = weatherItem.date.toString()
        hour_text.text = weatherItem.text.toString()
        hour_temp.text = weatherItem.temp_c.toString()
        hour_chance.text = weatherItem.chance.toString()

        Glide.with(App.instance)    // context
            .load(weatherItem.icon)  // 출력시킬 사진
            .placeholder(R.mipmap.ic_launcher) // 작동되지 않을 시 기본값 설정
            .into(hour_icon)   // View
    }
}