kequality
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.
Installation
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")
Usage examples
Custom equality check logic
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)
}
}
Complex declarative equality check
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
}
Check if two lists contain equal objects ignoring their order
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
Check if objects are almost the same (except a single property)
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
Simplify Android RecyclerView DiffUtil usage
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
Convert 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
)
Use HasDiffCallbackId
to make it even shorter
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()
)
Even shorter usage for simpler classes
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
)