
프로젝트에 관한 설명 및 코드는 아래 링크에 올려두었으니 참고 부탁드립니다.
📍프로젝트 설명 및 코드 바로가기
object AutoDeleteDiffUtil: DiffUtil.ItemCallback<BaseItem>() {
override fun areItemsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
return oldItem == newItem
}
}
AutoDeleteDiffUtil를 만드는 중 areContentsTheSame 함수의 == 에서 아래와 같은 에러가 발생하였습니다.
Suspicious equality check: equals() is not implemented in BaseItem
equals()가 BaseItem에서 구현되지 않았다는 동등성 검사 에러 메시지가 떠서 BaseItem 구현 부분을 확인해보기 전에.. DiffUtill에 대해 먼저 알아봅시다!
안드로이드에서 RecyclerView의 DiffUtil을 사용할 때, 아이템이 동일한지 판단하는 두 가지 메서드가 있습니다.
1. areItemsTheSame(oldItem, newItem)
2. areContentsTheSame(oldItem, newItem)
하나씩 확인해보면,
1. 아이템 자체가 동일한지 검사하는 메서드 : areItemsTheSame(oldItem, newItem)
여기에서 ID는 데이터베이스에서 Primary Key 같은 역할을 하는데
즉, 두 아이템이 같은 id를 가지면 동일한 아이템이라고 판단 합니다.
falsetrue2. 아이템의 내용이 동일한지 검사하는 메서드 : areContentsTheSame(oldItem, newItem)
areItemsTheSame에서 true가 나왔을 때만 호출됨id를 가졌더라도, 내용이 달라졌는지 확인true, 다르면 false예제 코드
override fun areContentsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
return oldItem == newItem
}
여기서 BaseItem이 data class라면, == 연산자는 자동으로 equals()를 호출해서 모든 필드를 비교해줍니다.
따라서 data class가 아니고, equals()가 구현되어있지 않으면 위에서 발생한 에러가 나타나게 됩니다.
그럼 어떨 때 false와 true를 반환할까요?
falsetrueDiffUtil이 비교를 시작newList가 들어오면, oldList와 비교
각 아이템을 areItemsTheSame으로 비교
false → 완전히 새로운 아이템으로 간주 → 뷰홀더 다시 생성
true → 같은 아이템이므로 areContentsTheSame 비교
areContentsTheSame을 확인
false → 기존 아이템이 변경된 것으로 간주 → 뷰만 업데이트 (애니메이션 적용)
true → 변경 사항 없음 → UI 업데이트 없음
interface BaseItem {
val id: Int
val name: String
}
data class OrdinaryItem(
override val id: Int,
override val name: String
) : BaseItem
data class AutoDeleteItem(
override val id: Int,
override val name: String,
val time: Int = 0
): BaseItem
BaseItem은 인터페이스라서 data class와는 달리 equals() 메서드가 자동으로 제공되지 않음.oldItem == newItem은 결국 두 객체가 같은 메모리 주소를 가리키는지를 비교할 뿐, 내부 값 비교를 하지 않음.BaseItem을 상속받은 OrdinaryItem과 AutoDeleteItem이 각각 equals()를 자동으로 가지더라도, BaseItem 타입으로 비교할 때는 이를 사용할 수 없음.interface BaseItem {
val id: Int
val name: String
override fun equals(other: Any?): Boolean
}
override fun areContentsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
return when {
oldItem is OrdinaryItem && newItem is OrdinaryItem -> oldItem as OrdinaryItem == newItem as OrdinaryItem
oldItem is AutoDeleteItem && newItem is AutoDeleteItem -> oldItem as AutoDeleteItem == newItem as AutoDeleteItem
else -> false
}
}
areContentsTheSame에서 id와 name을 직접 비교하기override fun areContentsTheSame(oldItem: BaseItem, newItem: BaseItem): Boolean {
return oldItem.id == newItem.id && oldItem.name == newItem.name
}
sealed interface 이용하기sealed interface BaseItem {
val id: Int
val name: String
}
abstract class 이용하기abstract class BaseItem(
open val id: Int,
open val name: String
)
총 5가지 방법을 찾았고, sealed interface와 abstract class의 차이점에 대해서도 찾아봤습니다.
| 특징 | sealed interface | abstract class |
|---|---|---|
| 다중 구현 | ✅ 가능 | ❌ 불가능 (단일 상속) |
| 공통 로직 포함 | ❌ 불가능 | ✅ 가능 (메서드/속성 포함 가능) |
| 생성자 | ❌ 없음 | ✅ 가능 |
| 타입 안정성 | ✅ when에서 else 없이 사용 가능 | ✅ when에서 else 없이 사용 가능 |
| 필드 강제 | ❌ 선택적으로 구현해야 함 | ✅ 강제됨 (open val 사용 가능) |
BaseItem 은 AutoDeleteAdapter에서 두 개의 뷰홀더를 구현하기 위해 추가한 인터페이스입니다. 또한, ainViewModel에서도 when 문을 활용하기 때문에 보일러플레이트 코드를 줄일 수 있어 sealed interface를 추가 하였습니다.