Custom equality checking utility.
It allows you to:
- Extract the custom equality checks to make them reusable
- Use different implementations of the equality check depending on the business context
- Write declarative and easily readable equality checking logic
- Simplify Android's RecyclerView DiffUtil usage
See Usage examples below.
First, make sure you have Maven Central in your repositories:
repositories {
mavenCentral()
}
Then, declare the dependency like:
// Base kequality features
implementation("dev.bright.kequality:kequality:1.5.0")
// Android's RecyclerView DiffUtil integration
implementation("dev.bright.kequality:diffutil:1.5.0")
data class Address(
val city: String
)
val AddressIgnoreCaseEquality = object : Equality<Address> {
override fun areEqual(o1: Address, o2: Address): Boolean {
return o1.city.equals(o2.city, ignoreCase = true)
}
}
data class Person(
val name: String,
val age: BigDecimal,
val address: Address
)
val PersonEquality = CompositeEquality(
Person::name.equalsEquality, // uses Any.equals()
Person::age.comparableEquality, // uses Comparable.compareTo()
Person::address.equalityBy(AddressIgnoreCaseEquality) // see above
)
or
val PersonEquality = Equality<Person> {
by { name } // uses Any.equals()
by(ComparableEquality()) { age } // uses Comparable.compareTo()
by(AddressIgnoreCaseEquality) { address } // see above
}
Regular equals
comparison of two lists containing equal objects ordered differently returns false
.
Using ListEquality
you can specify if the order has to be the same to consider the lists equal.
data class Person(val name: String)
val people1 = listOf(Person("Alice"), Person("Bob"))
val people2 = listOf(Person("Bob"), Person("Alice"))
people1 == people2 // returns false
val regularPersonEquality = EqualsEquality<Person>()
val peopleListEquality = ListEquality(regularPersonEquality, ignoreOrder = true)
peopleListEquality.areEqual(people1, people2) // returns true
You can easily find out if objects are almost the same by excluding a single property from the equality check.
All the other properties can be compared using the regular equals
method.
data class Person(val name: String, val age: Int, val height: Int)
val person1 = Person("Alice", 20, 175)
val person2 = Person("Bob", 20, 175)
val personEquality = CompositeEquality(
Person::class.declaredMemberProperties
.filterNot { it == Person::name }
.map { it.equalsEquality }
)
personEquality.areEqual(person1, person2) // returns true
Normally, when you use ListAdapter
, you prepare a class implementing
DiffUtil.ItemCallback<T>
, for example:
data class Person(
val id: Long,
val name: String,
val age: BigDecimal
)
class PersonDiffUtilItemCallback : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.name == newItem.name && oldItem.age.compareTo(newItem.age) == 0
}
}
The logic inside areItemsTheSame
and areContentsTheSame
is hardly
reusable and readable, especially when your class has a lot of
properties to compare.
Moreover, if you want to use DiffUtil.calculateDiff
to manually
calculate the diff between two collections, you must write yet
another class extending DiffUtil.Callback
like this:
class PersonDiffUtilCallback(val oldItems: List<Person>, val newItems: List<Person>) : DiffUtil.Callback() {
private val itemCallback = PersonDiffUtilItemCallback()
override fun getOldListSize(): Int = oldItems.size
override fun getNewListSize(): Int = newItems.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return itemCallback.areItemsTheSame(oldItems[oldItemPosition], newItems[newItemPosition])
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return itemCallback.areContentsTheSame(oldItems[oldItemPosition], newItems[newItemPosition])
}
}
and use it this way:
DiffUtil.calculateDiff(PersonDiffUtilCallback(items1, items2))
With kequality, you can easily convert any Equality<T>
into
DiffUtil.ItemCallback<T>
or DiffUtil.Callback
data class Person(
val id: Long,
val name: String,
val age: BigDecimal
)
val PersonIdEquality = Equality<Person> {
by { id }
}
val PersonContentEquality = Equality<Person> {
by { name }
by(ComparableEquality()) { age }
}
val PersonDiffUtilItemCallback: DiffUtil.ItemCallback<Person> =
DiffUtilDelegatingItemCallback(
diffUtilIdentityCheck = PersonIdEquality.diffUtilIdentityCheck(),
diffUtilContentCheck = PersonContentEquality.diffUtilContentCheck()
)
fun PersonDiffUtilCallback(oldItems: List<Person>, newItems: List<Person>): DiffUtil.Callback =
DiffUtilDelegatingCallback(
oldItems = oldItems,
newItems = newItems,
diffUtilDelegatingItemCallback = PersonDiffUtilItemCallback // or pass another diffUtilIdentityCheck and diffUtilContentCheck
)
If you let your class implement HasDiffCallbackId
interface like this:
data class Person(
val id: Long,
val name: String,
val age: BigDecimal
) : HasDiffCallbackId {
override val diffCallbackId: Any
get() = id
}
then the previous example can be further shortened to:
val PersonContentEquality = Equality<Person> {
by { name }
by(ComparableEquality()) { age }
}
val PersonDiffUtilItemCallback: DiffUtil.ItemCallback<Person> =
DiffItemCallbackById(
diffUtilContentCheck = PersonContentEquality.diffUtilContentCheck()
)
fun PersonDiffUtilCallback(oldItems: List<Person>, newItems: List<Person>): DiffUtil.Callback =
DiffCallbackById(
oldItems = oldItems,
newItems = newItems,
diffUtilContentCheck = PersonContentEquality.diffUtilContentCheck()
)
The examples above were based on the Person
class that required
customized equality check because age
was BigDecimal
so calling
equals()
was not good enough (because e.g. BigDecimal("10.0")
is not
equal to BigDecimal("10")
)
If your class doesn't need custom equality check and you can rely on
equals()
, e.g.
data class Person(
val id: Long,
val name: String
) : HasDiffCallbackId {
override val diffCallbackId: Any
get() = id
}
then creating the DiffUtil
implementations based on
EqualsEquality<T>
is even simpler:
val PersonDiffUtilItemCallback: DiffUtil.ItemCallback<Person> = DiffItemCallbackById()
fun PersonDiffUtilCallback(oldItems: List<Person>, newItems: List<Person>): DiffUtil.Callback =
DiffCallbackById(
oldItems = oldItems,
newItems = newItems
)