Data class 이해하고 RecyclerView에서 사용하기

이번 포스팅에서는 Data class를 사용하는 법에 대해 알아보도록 하겠습니다.

# Data class의 특징

# toString

코틀린에서 클래스를 만들 때는 자바의 클래스 스펙에 따라서 toString, equals, hashCode라는 메소드를 구현해주어야 합니다. 우선 Person이라는 클래스를 만들고 그 인스턴스를 출력해 보겠습니다. 그러면 Person@랜덤 이라는 값이 출력이 되게 됩니다.

1
2
3
4
5
6
7
class Person(var name: String, var age: Int, var sex: String)

val person1 = Person("Alice", 20, "Female")
println(person1)

// 출력값
org.jetbrains.kotlin.idea.scratch.generated.ScratchFileRunnerGenerated$ScratchFileRunnerGenerated$Person@b6587368

이제 클래스의 toString을 다음과 같이 오버라이드 해주면 랜덤값이 아니라 클래스가 가진 프로퍼티값을 보여주게 됩니다.

1
2
3
4
5
6
override fun toString(): String {
    return "Person(name=$name, age=$age, sex=$sex)"
}

// 출력값
Person(name=Alice, age=20, sex=Female)

# equals

equals는 같은 클래스로부터 인스턴스를 만들었을 때, 클래스 내부의 프로퍼티가 일치하면 같은 인스턴스로 취급할지를 판정하는 메소드입니다. 예를 들어 다음과 같은 인스턴스를 만들어 보면, 두 인스턴스는 동일하지 않다는 판정이 나오게 됩니다.

1
2
3
4
5
6
val person2 = Person("Bob", 22, "Male")
val person3 = Person("Bob", 22, "Male")
println(person2 == person3)

// 출력값
false

이제 equals를 다음과 같이 오버라이드 해줍니다. 비교대상이 되는 인스턴스를 받아서 그게 null이거나 Person 클래스가 아닐 경우엔 false를 반환하지만, 인스턴스의 프로퍼티가 일치하게 되면 동일한 객체라는 판정을 받게 됩니다.

1
2
3
4
5
6
7
8
override fun equals(other: Any?): Boolean {
    if (other == null || other !is Person)
        return false
    return name == other.name && age == other.age && sex == other.sex
}

// 출력값
true

# hashCode

hashCode는 인스턴스의 해시값을 정의하는 메소드입니다. 이 부분을 정의하지 않으면 equals의 값이 동일하더라도 서로 다른 객체가 되어버립니다.

1
2
3
4
5
val person4 = hashSetOf(Person("Alice", 20, "Female"))
println(person4.contains(Person("Alice", 20, "Female")))

// 출력값
false

그래서 hashCode를 정의하여 동일한 프로퍼티를 가질 경우 동일한 해시값을 갖도록 합니다.

1
2
3
4
5
6
override fun hashCode(): Int {
    return name.hashCode()  - 1234 + age - sex.hashCode()
}

// 출력값
true

# copy

copy는 이미 존재하는 인스턴스의 파라미터만을 바꿔서 새로운 객체를 만들어주는 메소드입니다. 이때 복사는 얕은복사로 이루어집니다.

1
2
val person5 = Person("Bob", 22, "Male")
val person6 = person5.copy(name = "Alice")  // Person(name=Alice, age=22, sex=Male)

# data class

그런데 그냥 프로퍼티만을 가지는 단순한 클래스의 경우 이런 것들을 다 정의해주는 행위가 번거로운 일이기 때문에, 코틀린에서는 Data class라고 하는 새로운 클래스를 준비했습니다. Data class를 사용하면 위에 언급한 메소드를 자동으로 정의하여 주기 때문에 다음과 같이 간단하게 클래스를 만들수 있게 됩니다.

사용하는 법은 매우 간단한데요, class 앞에 data라는 접두어를 붙여주기만 하면 됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
data class Person(var name: String, var age: Int, var sex: String)

val person1 = Person("Alice", 20, "Female")
println(person1)  // Person(name=Alice, age=20, sex=Female)

val person2 = Person("Bob", 22, "Male")
val person3 = Person("Bob", 22, "Male")
println(person2 == person3)  // true

val person4 = hashSetOf(Person("Alice", 20, "Female"))
println(person4.contains(Person("Alice", 20, "Female")))  // true

# RecyclerView의 데이터를 data class로 정의하기

그럼 data class를 RecyclerView에 적용해 보도록 하겠습니다.

여기서는 몬스터의 이름, 종족, 레벨, 그리고 몬스터의 능력치, 만난적이 있는지에 대한 Boolean값을 클래스가 가지도록 하고 그것을 Recyclerview에서 보여주도록 할 건데요, 기존에 다른 필요로 인해 만들어 두었던 RecyclerView를 Swipe, Drag, Touch하기 강의에서 작성했던 앱을 수정하면서 구현해보도록 하겠습니다.

우선은 몬스터 클래스를 하나 만들어 줍니다. 프로퍼티는 가능한 여러가지의 타입을 가지도록 했습니다. 종족은 선택하기 편하도록 enum을 도입했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
data class Monster(
        val name: String,
        val race: Race,
        val level: Int,
        val stats: List<Int>,
        val encount: Boolean
)

enum class Race {
    Zombie, Human, Goblin, Dragon
}

그리고 기존의 list_item.xml에서 두 개였던 텍스트뷰의 개수를 늘려줍니다. 위에서부터 순서대로 몬스터 클래스의 프로퍼티를 보여주도록 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?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"
    android:orientation="horizontal"
    android:paddingStart="20dp"
    android:paddingEnd="20dp"
    android:id="@+id/vhLayout"
    android:background="?attr/selectableItemBackground"
    android:gravity="center_vertical">

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="9"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Monster name"
            android:textSize="24sp"
            android:padding="3dp" />

        <TextView
            android:id="@+id/tv_race"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Race"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/tv_level"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Level"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/tv_stats"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Stats"
            android:textSize="18sp" />

        <TextView
            android:id="@+id/tv_encount"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Encount"
            android:textSize="18sp" />
    </LinearLayout>

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1">

        <ImageView
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_gravity="center"
            android:src="@drawable/ic_baseline_reorder_24" />

    </FrameLayout>
</LinearLayout>

다음은 RecyclerView Adapter에서 사용하는 데이터셋의 타입을 바꾸고 다른 메소드들도 변화시킵니다. 기존의 setData는 필요없어졌기 때문에 삭제하고, ViewHolder에서 데이터와 텍스트뷰를 바인딩하는 부분도 바뀐 타입에 맞도록 수정해 줍니다. 그리고 데이터를 추가하는 기능을 가진 addData 메소드를 추가해줍니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class RecyclerViewAdapter: RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
    private val dataSet: ArrayList<Monster> = arrayListOf()

    fun removeData(position: Int) {
        dataSet.removeAt(position)
        notifyItemRemoved(position)
    }

    fun swapData(fromPos: Int, toPos: Int) {
        Collections.swap(dataSet, fromPos, toPos)
        notifyItemMoved(fromPos, toPos)
    }

    fun addData(name: String, race: Race, level: Int, stats: List<Int>, encount: Boolean) {
        dataSet.add(Monster(name, race, level, stats, encount))
        notifyItemInserted(dataSet.size)
    }

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

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

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

    inner class ViewHolder(private val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(data:Monster) {
            binding.tvName.text = "Name: ${data.name}"
            binding.tvRace.text = "Race: ${data.race}"
            binding.tvLevel.text = "Level: ${data.level}"
            binding.tvStats.text = "HP: ${data.stats[0]} / MP: ${data.stats[1]} / Exp: ${data.stats[2]}"
            binding.tvEncount.text = "Encounted: ${data.encount}"

            binding.vhLayout.setOnClickListener {
                Snackbar.make(it, "Item $layoutPosition touched!", Snackbar.LENGTH_SHORT).show()
            }
        }
    }
}

마지막으로 addData 메소드로 MainActivity에서 데이터값을 만들어주면 됩니다.

1
2
3
4
5
6
7
8
9
    override fun onCreate(savedInstanceState: Bundle?) {
...
        rvAdapter.addData("타일런트", Race.Zombie, 10, listOf(100,10,50), false)
        rvAdapter.addData("조커", Race.Human, 23, listOf(200,20,100), false)
        rvAdapter.addData("그렘린", Race.Goblin, 2, listOf(10,1,5), true)
        rvAdapter.addData("리오레우스", Race.Dragon, 2500, listOf(10000,1000,50000), false)
        rvAdapter.addData("사우론", Race.Human, 100, listOf(1000,200,1000),false)
        rvAdapter.addData("리바이어던", Race.Dragon, 50, listOf(2000,250,10000), true)
    }

이렇게 해서 data class의 특징을 이해하고, RecyclerView에서 활용하는 법에 대해 알아보았습니다.

Built with Hugo
Theme Stack designed by Jimmy