[Kotlin] sealed class
Kotlin의 sealed class는 계층 구조를 안전하게 만들어준다. 즉, sealed class에 상속된 sub class들이 무엇이 있는 지 컴파일 타임에 알 수 있다. 이 말이 무슨 뜻일지 파헤쳐보자.
먼저 abstract class와 이를 일반화하는 클래스들을 살펴보자. when
구문을 이용하면 else 분기가 꼭 필요하다. 없다면 컴파일러가 에러를 보인다.
abstract class Foo
class Bar : Foo()
class Baz : Foo()
// ...
fun render(foo: Foo) {
// COMPILE ERROR!
val some = when (foo) { // -> 'when' expression must be exhaustive, add necessary 'else' branch
is Bar -> { /* ... */ }
is Baz -> { /* ... */ }
}
}
이와 다르게 sealed class는 상속된 sub class들이 무엇이 있는 지 컴파일 타임에 알 수 있기 때문에 아래와 같은 구문이 가능하다.
sealed class Foo {
class Bar: Foo()
class Baz: Foo()
}
// ...
fun render(foo: Foo) {
val some = when (foo) {
is Foo.Bar -> { /* ... */ }
is Foo.Baz -> { /* ... */ }
}
}
그렇다면 enum class와 비교하였을 때 무엇이 다를까? 먼저 enum은 각 값들이 상수 값들이다. 이는 enum 클래스를 상속할 수 없다는 것을 의미하고 인스턴스를 생성할 수 없다는 것을 의미한다.
enum class Foo {
BAR,
BAZ
}
// COMPILE ERROR!
class Child : Foo() // This type is final, so it cannot be inherited from
fun some() {
// COMPILE ERROR!
val foo = Foo() // Enum types cannot be instantiated
}
이와 다르게 sealed class는 자식 클래스들의 인스턴스를 만들 수 있으며 동일한 패키지 내에서 상속이 가능하다.
sealed class Foo {
open class Bar: Foo()
}
class Baz : Foo.Bar()
fun some() {
val bar = Foo.Bar()
}
Usage
이 sealed 클래스가 유용하게 사용되는 곳은 아래의 두 경우이다.
- UiState를 sealed class로 나타내는 경우
- RecyclerView의 Adapter에 다양한 타입을 제공하는 경우
UiState
MVVM 패턴을 이용할 때 UiState를 sealed class를 이용하여 구현할 수 있다. (아래의 구조는 각 상태가 변경되면 이전 상태를 별도로 보관하지 않는 한 이전 상태에 대한 데이터를 복구할 방법이 없다는 점이 단점이다. https://medium.com/@laco2951/android-ui-state-modeling-어떤게-좋을까-7b6232543f25)
sealed class MainUiState {
object Loading : MainUiState()
data class Success(val name: String) : MainUiState()
data class Error(val exception: Throwable) : MainUiState()
}
위의 클래스를 이용하여 뷰에서는 아래와 같이 처리할 수 있다.
fun updateUi(uiState: MainUiState) {
when (uiState) {
is MainUiState.Success -> {
// ...
}
is MainUiState.Error -> {
// ...
}
Loading -> {
// ...
}
}
}
Recycler View
리사이클러 뷰에 하나의 뷰홀더가 아닌 다양한 타입의 뷰홀더가 필요한 경우 sealed class가 제격이다.
아래와 같이 PostUiModel
, PostViewHolder
를 sealed class를 이용하면 타입을 안정적으로 구현할 수 있다.
sealed class PostUiModel(val viewType: Int) {
companion object {
const val HEADER_VIEW_TYPE = 0
const val ITEM_VIEW_TYPE = 1
const val FOOTER_VIEW_TYPE = 2
}
data class Header(val title: String) : PostUiModel(HEADER_VIEW_TYPE)
data class Item(val title: String, val author: String) : PostUiModel(ITEM_VIEW_TYPE)
data class Footer(val copyright: String) : PostUiModel(FOOTER_VIEW_TYPE)
}
val posts: List<PostUiModel> = listOf(
PostUiModel.Header(title = "공지사항"),
PostUiModel.Item(title = "1월 15일 모임", author = "김민성"),
PostUiModel.Item(title = "1월 8일 모임", author = "김민성"),
PostUiModel.Item(title = "1월 1일 모임", author = "김민성"),
PostUiModel.Header(title = "추천목록"),
PostUiModel.Item(title = "Jetpack Compose 탐험", author = "김민성"),
PostUiModel.Item(title = "RecyclerView 파헤치기", author = "김민성"),
PostUiModel.Footer(copyright = "jja08111")
)
sealed class PostViewHolder(
binding: ViewBinding
) : ViewHolder(binding.root) {
class Header(
private val binding: ItemHeaderBinding
) : PostViewHolder(binding) {
fun bind(item: PostUiModel.Header) {
binding.headerTitle.text = item.title
}
}
class Item(
private val binding: ItemPostBinding
) : PostViewHolder(binding) {
fun bind(item: PostUiModel.Item) {
binding.title.text = item.author
binding.author.text = item.author
}
}
class Footer(
private val binding: ItemFooterBinding
) : PostViewHolder(binding) {
fun bind(item: PostUiModel.Footer) {
binding.copyright.text = item.copyright
}
}
}
class PostAdapter : ListAdapter<PostUiModel, PostViewHolder>(DIFF) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
return when (viewType) {
PostUiModel.HEADER_VIEW_TYPE -> PostViewHolder.Header(
ItemHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
PostUiModel.ITEM_VIEW_TYPE -> PostViewHolder.Item(
ItemPostBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
PostUiModel.FOOTER_VIEW_TYPE -> PostViewHolder.Footer(
ItemFooterBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException()
}
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
val item = getItem(position)
when (holder) {
is PostViewHolder.Header -> holder.bind(item as PostUiModel.Header)
is PostViewHolder.Item -> holder.bind(item as PostUiModel.Item)
is PostViewHolder.Footer -> holder.bind(item as PostUiModel.Footer)
}
}
override fun getItemViewType(position: Int): Int {
return getItem(position).viewType
}
companion object {
private val DIFF = object : DiffUtil.ItemCallback<PostUiModel>() {
override fun areItemsTheSame(oldItem: PostUiModel, newItem: PostUiModel): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: PostUiModel, newItem: PostUiModel): Boolean {
return oldItem == newItem
}
}
}
}
댓글남기기