본문 바로가기
📱 Android

[Android] RecyclerView (1) - Multi ViewHolder

by 콩드로이드 2021. 5. 15.

안녕하세요 :) 

오늘은 RecyclerView를 사용해보겠습니다 

 

굉장히 자주 쓰이는 부분이기에 저 또한 더 자세히 알아두고자 정리해보려 합니다.

(사용 빈도는 높지만 프로젝트를 처음부터 구현하지 않으면, 잘 잊기 쉽더라구요)

 

단순한 RecyclerView 사용보다 API를 연동해서 사용하는 방법을 정리해두면 실무에 더 될 것 같아서

REST API, ViewHolder, Retrofit, OkHttp를 함께 사용해보겠습니다.

 

RecyclerView란 기존에 목록을 나타내기 위해 사용했던 ListView보다 더 유연하고 향상된 View로 쉽게 말하자면 ListView의 상위 버전이라고 생각하면 좋을 것 같습니다 


1. 라이브러리 추가하기

 

implementation 'com.google.code.gson:gson:2.8.5'

implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'

implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'

implementation 'androidx.recyclerview:recyclerview:1.2.0'

 

각 라이브러리를 build.gradle[app]에 명시해줍니다

(2021.05월 기준 최신 라이브러리 버전입니다)

 

 

2. AndroidManifest.xml 권한 설정

 

인터넷에 접속해야 작업이 가능하기 때문에 권한을 지정해줍니다.

<uses-permission android:name="android.permission.INTERNET" />  

 

3. DataModel 지정하기 (Parcelable)

 

데이터를 가져오기 위해 Model을 정해주겠습니다. Model을 사용하지 않고도 가능하지만, 프로젝트 규모가 커질 경우 모델링을 사용하는 것이 편리합니다 :) 

Parcelable을 extends해서 사용하려고 합니다.


Parcelable을 사용하기 전, 간단히 개념에 대해 짚고 넘어가보겠습니다

 

📌 Parcelable : 안드로이드에서 사용하는 객체 직렬화 방법 중 하나로 Android SDK Interface로 사용되는 함수들은 아래와 같습니다

 

writeToParcel() : 직렬화를 사용해 객체에 저장

createFromParcel() : 전달받은 데이터를 순차로 읽으며 역직렬화합니다

 

 

data class MainModel(
    val type: Int,
    val img:String,
    val title: String,
    val timestamp: String
) : Parcelable {
    constructor(parcel: Parcel) : this(
        parcel.readInt(),
        parcel.readString()!!,
        parcel.readString()!!,
        parcel.readString()!!
    )

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeInt(type)
        parcel.writeString(img)
        parcel.writeString(title)
        parcel.writeString(timestamp)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<MainModel> {
        override fun createFromParcel(parcel: Parcel): MainModel {
            return MainModel(parcel)
        }

        override fun newArray(size: Int): Array<MainModel?> {
            return arrayOfNulls(size)
        }
    }
}

 

 

4. Item들의 Layout 생성

 

RecyclerView에는 여러가지 Item을 넣을 수 있는데, 예제에선 2가지 Item을 사용하도록 하겠습니다

이해가 가기 쉽게 사진을 첨부하겠습니다 (만들고자 하는 예시 화면입니다)

위 사진을 보시면 상위 부분에 Item1, 하위 목록에 Item2로 구분을 했습니다. 총 사용되는 ViewHolder는 2개가 되겠죠 😊

 

[Item1]

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:layout_marginTop="38dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#000000"
        android:layout_marginStart="20dp"
        android:layout_marginLeft="20dp"
        android:textSize="38dp"
        android:layout_marginBottom="40dp"
        android:id="@+id/tvTop"
        android:textStyle="bold"
        android:text=""
        />
</LinearLayout>

 

[Item2]

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tvImg"
        android:layout_marginBottom="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="20dp"
        android:text=""
        android:textSize="40dp" />

    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="20dp"
        android:layout_toRightOf="@id/tvImg"
        android:text=""
        android:textColor="#000000"
        android:textSize="18dp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tvStamp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tvTitle"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="5dp"
        android:layout_toRightOf="@id/tvImg"
        android:text=""
        android:textColor="#000000"
        android:textSize="13dp" />
</RelativeLayout>

 

 

 

5. ViewHolder, Adapter생성

 

Layout을 생성했으면 코드와 xml을 연결시키는 작업이 필요합니다

 

위에서 설명했지만 ViewHolder는 2개가 사용되고,

Adapter는 하나로 하든, ViewHolder 갯수에 맞춰 사용하든 상관없지만 이해를 돕기 위해 1개만 사용하겠습니다

ViewHolder의 갯수가 더 늘어난다면 따로 만드는 것이 더 보기 편합니다


 

1) 📌 ViewHolder : 개별 항목의 레이아웃을 포함하는 View의 래퍼 즉, 각 선언된 View를 재활용하는 부분입니다

 

각 function의 자세한 정의는 안드로이드 개발자 페이지를 참조하겠습니다. 

 

1️⃣ onCreateViewHolder()

RecyclerView는 ViewHolder를 새로 만들어야 할 때마다 이 메서드를 호출. 이 메서드는 ViewHolder와 그에 연결된 View를 생성하고 초기화하지만 뷰의 콘텐츠를 채우지는 않습니다. ViewHolder가 아직 특정 데이터에 바인딩된 상태가 아니기 때문입니다.

 

2️⃣onBindViewHolder()

RecyclerView는 ViewHolder를 데이터와 연결할 때 이 메서드를 호출합니다. 이 메서드는 적절한 데이터를 가져와서 그 데이터를 사용하여 뷰 홀더의 레이아웃을 채웁니다. 

 

3️⃣getItemCount()

RecyclerView는 DataSet의 크기를 가져올 때 이 메서드를 호출합니다.  RecyclerView는 이 메서드를 사용하여, 항목을 추가로 표시할 수 없는 상황을 확인합니다.

 

 

2) 📌 Adapter : ViewHolder 객체를 생성하고, 뷰와 데이터를 연결합니다

 

2개의 ViewHolder를 int값으로 비교해서 각각 객체를 생성해줍니다 

 

class MainAdapter(var datas: ArrayList<MainModel>) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    val TOP = 0
    val ITEM = 1

    override fun getItemViewType(position: Int): Int {
        return datas[position].type
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view: View?
        return when (viewType) {
            TOP -> {
                view = LayoutInflater.from(parent.context).inflate(R.layout.top_main, parent, false)
                TopViewHolder(view)
            }
            ITEM -> {
                view =
                    LayoutInflater.from(parent.context).inflate(R.layout.item_main, parent, false)
                ItemViewHolder(view)
            }
            else -> {
                view = LayoutInflater.from(parent.context).inflate(R.layout.top_main, parent, false)
                TopViewHolder(view)
            }
        }
    }

    override fun getItemCount() = datas.size

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val viewItem = datas[position]
        when (viewItem.type) {
            0 -> {
                holder as TopViewHolder
                holder.bind(viewItem)
                holder.setIsRecyclable(false)
            }
            1 -> {
                holder as ItemViewHolder
                holder.bind(viewItem)
                holder.setIsRecyclable(false)
            }
        }
    }

    inner class TopViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var tvUser = itemView.tvTop
        fun bind(item: MainModel) {
            val user = item.title
            tvUser.text = "\uD83D\uDE00$user 님\uD83C\uDF40\n반갑습니다"
        }
    }

    inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var tvTitle = itemView.tvTitle
        var tvImg = itemView.tvImg
        var tvStamp = itemView.tvStamp
        fun bind(item: MainModel) {
            tvImg.text = item.img
            tvTitle.text = item.title
            tvStamp.text = item.timestamp
        }
    }
}

 

 

6. Activity

 

이제 RecyclerView를 쓸 준비가 끝나갑니다🙌🙌

 

Adapter에 데이터를 전달해주는 역할을 담당하는데, API로 데이터를 가져오기 전에 간단한 테스트 코드로

위의 RecyclerView 준비가 잘 끝났는지 확인해보도록 하겠습니다 😎

 

R.layout.activity_main 엔 RecyclerView 1개만 존재하는 예제입니다

 

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initView()
    }

    fun initView() {
        var list = ArrayList<MainModel>()
        list.add(MainModel(type = 0, img = "😎", title = "UserName", timestamp = "2021-05-15"))
        list.add(
            MainModel(
                type = 1,
                img = "💦",
                title = "교통카드를 두고 와서 당황했..",
                timestamp = "2021-05-15 14:05:00"
            )
        )
        list.add(
            MainModel(
                type = 1,
                img = "🍝",
                title = "오늘만 지나면 주말 로제떡볶..",
                timestamp = "2021-05-14 17:52:00"
            )
        )

        val adapter = MainAdapter(list)

        recyclerView.layoutManager = GridLayoutManager(this, 1)
        recyclerView.adapter = adapter
        adapter.notifyDataSetChanged()
    }
}

 

해당 소스를 넣고 앱을 구동하면 아래와 같이 나타납니다 

(단순 예제를 보여주기 위한 테스트임으로 TextView Ellipsize는 적용하지 않았습니다)

 

API 연동 없이 사용하실 분들은 해당 포스팅만 참조하시면 됩니다

REST API를 연동해서 데이터를 받아오실 분은 다음 포스팅에서 뵙겠습니다 

 

궁금하신 점이나 의견이 있으시면 댓글 부탁드립니다 감사합니다 😊