안드로이드 스터디

RecyclerView를 써 보자!

mky 2025. 11. 2. 14:36

설명할 미션 : 홈 화면(HomeFragment)에 앨범 RecyclerView 레이아웃 및 RecylcerView 만들기

 

우선 리사이클러뷰 구현은 간단하지 않다.. ㅈㄴ 어렵다 그래서 우선 구현 순서를 간략하게 써 보았다.

1. 리사이클러뷰가 보일 위치에 리사이클러뷰를 추가(HomeFragment에 RecyclerView를 추가)

2, 리사이클러뷰에 보일 아이템들의 뷰 레이아웃 구성(item_rv_album.xml)

3. 아이템 뷰 객체들에 들어갈 데이터리스트 생성(Album Data Class를 만들어서 ArrayList에 달아줌)

4. 어댑터 및 뷰홀더 구현(AlbumRVAdapter 및 ViewHolder Inner Class 생성)

5. 리사이클러뷰에 어댑터 연결 및 레이아웃 매니저 추가

그럼 이제 차근차근 한 단계씩 해 보자.

 

1. 리사이클러뷰가 보일 위치에 리사이클러뷰를 추가

fragment_home.xml에 다음과 같은 코드를 추가한다.

<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/home_today_music_album_rv"
            android:layout_width="match_parent"
            android:layout_height="210dp"
            android:layout_marginTop="10dp"
            app:layout_constraintTop_toBottomOf="@+id/home_today_music_total_tv"
            tools:listitem="@layout/item_album"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            android:orientation="horizontal" />

속성 중 tools:listitem속성을 사용하면 디버깅 시 용이하다.

 

2. 리사이클러뷰에 보일 아이템들의 뷰 레이아웃 구성

다음과 같이 item_rv_album.xml를 만들어 준다.

 

3. 아이템 뷰 객체들에 들어갈 데이터리스트 생성

우선 데이터 클래스를 만들자.

package com.example.flo.dataclasses

data class Album(
    var title: String? = "",
    var singer: String? = "",
    var coverImage: Int? = null,
    // 수록곡 정보용 arraylist
    var songs: ArrayList<Song>? = arrayListOf()
)

data class Song(
    val num : String? = "01",
    val title: String = "",
    val singer: String = "",
    //val img: Int = R.drawable.img_album_exp2
)

 

그 뒤 홈프래그먼트에 arraylist를 하나 선언해 준다.

private var albumDatas = ArrayList<Album>()

그 뒤 데이터를 담아준다.

// 앨범 데이터 리스트 생성 더미 데이터
        albumDatas.apply {
            add(
                Album(
                    title = "Butter",
                    singer = "방탄소년단 (BTS)",
                    coverImage = R.drawable.img_album_exp,
                    songs = arrayListOf(
                        Song("01", "Butter", "방탄소년단 (BTS)"),
                        Song("02", "Permission to Dance", "방탄소년단 (BTS)"),
                        Song("03", "Butter (Instrumental)", "방탄소년단 (BTS)"),
                        Song("04", "Permission to Dance (Instrumental)", "방탄소년단 (BTS)")
                    )
                )
            )
            add(
                Album(
                    title = "Lilac",
                    singer = "아이유 (IU)",
                    coverImage = R.drawable.img_album_exp2,
                    songs = arrayListOf(
                        Song("01", "라일락", "아이유 (IU)"),
                        Song("02", "Flu", "아이유 (IU)"),
                        Song("03", "Coin", "아이유 (IU)"),
                        Song("04", "봄 안녕 봄", "아이유 (IU)"),
                        Song("05", "Celebrity", "아이유 (IU)"),
                        Song("06", "돌림노래", "아이유 (IU)"),
                        Song("07", "빈 컵", "아이유 (IU)"),
                        Song("08", "아이와 나의 바다", "아이유 (IU)"),
                        Song("09", "어푸 (Ah puh )", "아이유 (IU)"),
                        Song("10", "에필로그", "아이유 (IU)")
                    )
                )
            )
            add(
                Album(
                    title = "Temp",
                    singer = "김시선 (UMC)",
                    coverImage = R.drawable.img_potcast_exp
                )
            )
            add(
                Album(
                    title = "Classic",
                    singer = "베토벤 (Beethoven)",
                    coverImage = R.drawable.img_first_album_default,
                    songs = arrayListOf(
                        Song("01", "월광 소나타", "베토벤 (Beethoven)"),
                        Song("02", "비창 소나타", "베토벤 (Beethoven)"),
                        Song("03", "운명 교향곡", "베토벤 (Beethoven)"),
                        Song("04", "영웅 교향곡", "베토벤 (Beethoven)"),
                        Song("05", "환희의 송가", "베토벤 (Beethoven)"),
                        Song("06", "황제 협주곡", "베토벤 (Beethoven)")

                    )
                )
            )
        }

이런 식으로 앨범 객체들을 담아준다.

 

4. 어댑터 및 뷰홀더 구현(AlbumRVAdapter 및 ViewHolder Inner Class 생성)

우선 어댑터 클래스를 만들어 보자. 전체 코드부터 봐보자.

package com.example.flo.home

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.flo.databinding.ItemAlbumBinding
import com.example.flo.dataclasses.Album

class AlbumRVAdapter(private var albumList: ArrayList<Album>): RecyclerView.Adapter<AlbumRVAdapter.ViewHolder>() {

    interface MyItemClickListener {
        fun onItemClick(album: Album)
//        fun onRemoveAlbum(position: Int)
    }
    private lateinit var myItemClickListener: MyItemClickListener
    fun setMyItemClickListener(itemClickListener: MyItemClickListener) {
        myItemClickListener = itemClickListener
    }

    fun addItem(album: Album) {
        albumList.add(album)
        notifyDataSetChanged()
    }

//    fun removeItem(position: Int) {
//        albumList.removeAt(position)
//        notifyDataSetChanged()
//    }

    override fun onCreateViewHolder(
        viewGroup: ViewGroup,
        viewType: Int
    ): ViewHolder {
        val binding: ItemAlbumBinding = ItemAlbumBinding.inflate(
            LayoutInflater.from(viewGroup.context), viewGroup, false
        )

        //아이템 뷰 객체를 던져줌
        return ViewHolder(binding)
    }

    // 뷰홀더에 데이터를 바인딩해줄때마다 호출, 클릭 이벤트 작성
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(albumList[position])
        holder.itemView.setOnClickListener { myItemClickListener.onItemClick(albumList[position]) }
//        holder.binding.itemAlbumTitleTv.setOnClickListener { myItemClickListener.onRemoveAlbum(position) }

    }

    //데이터 세트 크기를 알려줌. 리사이클러뷰의 끝이 어딘지
    override fun getItemCount(): Int = albumList.size

    //뷰홀더 클래스
    inner class ViewHolder(var binding: ItemAlbumBinding): RecyclerView.ViewHolder(binding.root) {

        fun bind(album: Album) {
            binding.itemAlbumTitleTv.text = album.title
            binding.itemAlbumSingerTv.text = album.singer
            binding.itemAlbumCoverImgIv.setImageResource(album.coverImage!!)
        }

    }

}

일단 저기서  CRUD관련 코드는 무시하고 하나씩 차근차근 해보자.

 

우선 어댑터가 아이템 뷰 객체들에게 데이터를 바인딩 해줘야 한다. 이를 위해 클래스의 인자로 아까 만든 데이터 리스트를 넣는다.

그리고 어댑터 제네릭 클래스를 상속받는다.<>안에는 뷰홀더를 넣는데, 아직 뷰홀더를 안만들어서 오류가 뜨겠지만 일단 넣는다.

class AlbumRVAdapter(private var albumList: ArrayList<Album>): RecyclerView.Adapter<AlbumRVAdapter.ViewHolder>()

이 코드를 이해하려면 제네릭 문법에 대해 알아야 한다. 제네릭은 클래스나 함수에서 타입을 파라미터로 받는 문법으로, RecyclerView.Adapter 클래스를 상속받을 때 어떤 타입의 뷰홀더를 사용할 지 지정해줘야 한다.

이제 필수 메서드 3개와 뷰홀더 클래스를 구현해줘야 한다. 뷰홀더부터 inner class로 구현해 보자.

//뷰홀더 클래스
    inner class ViewHolder(var binding: ItemAlbumBinding): RecyclerView.ViewHolder(binding.root) {

        fun bind(album: Album) {
            binding.itemAlbumTitleTv.text = album.title
            binding.itemAlbumSingerTv.text = album.singer
            binding.itemAlbumCoverImgIv.setImageResource(album.coverImage!!)
        }
    }

뷰홀더 클래스는 매개변수로 아이템 객체를 받는다. 그리고 뷰홀더 클래스를 상속받는다.

이제 3개의 필수 메서드를 구현해 보자.

override fun onCreateViewHolder(
        viewGroup: ViewGroup,
        viewType: Int
    ): ViewHolder {
        val binding: ItemAlbumBinding = ItemAlbumBinding.inflate(
            LayoutInflater.from(viewGroup.context), viewGroup, false
        )

        //아이템 뷰 객체를 던져줌
        return ViewHolder(binding)
    }

    // 뷰홀더에 데이터를 바인딩해줄때마다 호출, 클릭 이벤트 작성
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(albumList[position])
        holder.itemView.setOnClickListener { myItemClickListener.onItemClick(albumList[position]) }
//        holder.binding.itemAlbumTitleTv.setOnClickListener { myItemClickListener.onRemoveAlbum(position) }

    }

    //데이터 세트 크기를 알려줌. 리사이클러뷰의 끝이 어딘지
    override fun getItemCount(): Int = albumList.size
  • onCreateViewHolder는 뷰홀더를 생성해줘야 할 때 호출되는 함수이다. 
  • inflate함수로 xml레이아웃을 view객체로 변환(binding)한 뒤 그거로 만든 뷰홀더를 반환하게 한다.
  • onBindViewHolder는 뷰홀더에 데이터를 바인딩해줄때마다 호출되는 메서드로, CRUD클릭 이벤트 등을 작성한다.
  • 뷰홀더와 position을 매개변수로 받는데, position은 뷰페이저의 그것과 유사하다.
  • onBindViewHolder를 구현하기 전, 뷰홀더 클래스에서 bind함수를 정의해 준다. 
  • bind함수는 Album객체를 받아서, 그 안의 데이터를 아이템 뷰에 직접 연결해준다.
  • onBindViewHolder에서 bind함수를 호출해서 position에 해당하는 데이터를 리스트에서 꺼내서 뷰 홀더의 멤버로 전달해 준다.
  • getItemCount는 데이터 세트 크기를 알려준다. 리사이클러뷰의 끝이 어딘지를 알려준다. 리스트의 크기를 반환하게 한다.

5. 리사이클러뷰에 어댑터 연결 및 레이아웃 매니저 추가

HomeFragment.kt에 다음과 같은 코드를 추가한다.

//리사이클러뷰 어댑터 등록 (앨범 나열)
        val albumRVAdapter = AlbumRVAdapter(albumDatas)
        binding.homeTodayMusicAlbumRv.adapter = albumRVAdapter

위에서 만든 어댑터 클래스로 객체를 만들어주고 그걸 Rv의 속성으로 지정해 준다.

그 뒤 다음과 같은 코드를 추가한다.

//리사이클러뷰의 레이아웃 매니저 설정
        binding.homeTodayMusicAlbumRv.layoutManager = LinearLayoutManager(
            context,
            LinearLayoutManager.HORIZONTAL, false
        )

여기서 false는 reserveLayout옵션으로, 아이템 순서를 반대로 할지 여부이다.

  • false -> 일반 순서(왼쪽 -> 오른쪽)
  • true -> 반대 순서(오른쪽 -> 왼쪽)

여기서 의문점이 하나 생겼다. 이미 리사이클러뷰 위젯에서

app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"

이렇게 레이아웃매니저 속성값을 줬었다. 근데 왜 또 따로 설정하는 걸까?

그 이유는 방향 설정 때문이다. 바로 위처럼 레이아웃 매니저를 설정해놓으면 디폴트는 세로 방향이다. 하지만 가로 방향으로 배치되는 걸 원하기 때문에, 따로 설정해준 것이다.