Search code examples
androidkotlinsortingreflectioncomparator

How to sort a list based on multiple fields of sublcass dynamically


I am creating a Android Application which show list of Stores. The main Recyclerview shows the list of Store and every store object has SortingData which holds multiple fields like minimumDistance,rating etc.

By user selection i get a list of selected tags which size varies on the base of user selection and want to sort the main list of stores by it.

Every selected tag holds a propery isAscending which shows that it should be sorted in Ascending or Descending order. Lets say Rating will be Descending and Minimum Distance will be ascending and so on.

I have written a custom comparator to do so, to avoid multiple if conditions inside a loop but this comparator has issues. Like, its sorting the intergers based on first digit only doubles are also not sorted well.

Below is my code

data class Store(
    val name: String,
    val sortingData: SortingData,
    val status: String
) {
}
data class SortingData(
    val averageProductPrice: Int,
    val bestMatch: Double,
    val deliveryCosts: Int,
    val distance: Int,
    val minCost: Int,
    val newest: Double,
    val popularity: Double,
    val ratingAverage: Float
)

data class SortTag(var text: String, var key:String,var isSelected: Boolean,var isAscending:Boolean) {
}

Function

fun sortListByAdditionalTags(
    list: MutableList<Store>>,    selectedTags: List<SortTag>
): MutableList<Store> {
    /*
 Best Match -> Descending highest value on top
 Newest -> Descending highest value on top
 Rating Average -> Descending highest value on top
 Distance -> Ascending lowest value on top
 Popularity -> Descending highest value on top
 Average Product Price ->  Ascending lowest value on top
 Delivery cost ->  Ascending lowest value on top
 Min cost->  Ascending lowest value on top
 */
    var sorted = list
    selectedTags.forEach {
        sorted = list.sortedWith(
            comparator = AdditionalSortComparator(
                it.key,
                it.isAscending
            )
        ) as MutableList< Store 
>

    }
    return sorted

}

Custom Sort Comparator

class AdditionalSortComparator(
    private val sortProperty: String? = null,
    private val isAscending: Boolean
) : Comparator<Store> {
    override fun compare(o1: Store?, o2: Store?): Int {
        val sortingData =
            Store::class.declaredMemberProperties.firstOrNull { it.name == "sortingData" }

        val s1: sortingData = o1?.let { sortingData?.get(it) } as sortingData
        val s2: sortingData = o2?.let { sortingData?.get(it) } as sortingData

        val calledVariable = SortingData::class.declaredMemberProperties.firstOrNull { it.name == sortProperty }
        return if (calledVariable != null) {
            if (calledVariable.get(s1) is Int) {
                val valueFirst = calledVariable.get(s1) as Int
                val valueSecond = calledVariable.get(s2) as Int
                if (isAscending) valueFirst - valueSecond else valueSecond - valueFirst
            } else if (calledVariable.get(s1) is Float) {
                val valueFirst = calledVariable.get(s1) as Float
                val valueSecond = calledVariable.get(s2) as Float
                if (isAscending) valueFirst - valueSecond else valueSecond - valueFirst
            } else if (calledVariable.get(s1) is Double) {
                val valueFirst = calledVariable.get(s1) as Double
                val valueSecond = calledVariable.get(s2) as Double
                if (isAscending) abs(valueFirst-valueSecond) else abs(valueSecond-valueFirst)
            }

            if (isAscending) calledVariable.get(s1).toString()
                .compareTo(calledVariable.get(s2).toString())
            else calledVariable.get(s2).toString()
                .compareTo(calledVariable.get(s1).toString())
        } else {
            val idProperty = Store::name
            val valueFirst = idProperty.get(o1)
            val valueSecond = idProperty.get(o2)
            if (isAscending) valueFirst.compareTo(valueSecond) else valueSecond.compareTo(valueFirst)
        }
    }
}

The dependency i used for Kotlin Reflection is

implementation("org.jetbrains.kotlin:kotlin-reflect:1.7.10")

Can somebody please help me out with this, how i can achieve this functionality in an efficient and correct manner?


Solution

  • Never cast a List to a MutableList. It is inherently unsafe and can cause crashes at runtime. Even if it doesn't now, it could crash in the future if the standard library changes what kind of list implementation it's using under the hood. (If you truly have to have a MutableList, use toMutableList() to create a mutable copy)

    You should use MutableLists with a RecyclerView anyway...it can lead to all kinds of tricky bugs.

    The above alone might be part of your issue. The rest probably has to do with using reflection and string values of your non-string comparable properties.

    Here's how I would do it with minimal reflection.

    First, keep a map of the tags to the associated properties. This is the only line of reflection you need to do your task.

    data class SortingData(
        val averageProductPrice: Int,
        val bestMatch: Double,
        val deliveryCosts: Int,
        val distance: Int,
        val minCost: Int,
        val newest: Double,
        val popularity: Double,
        val ratingAverage: Float
    ) {
        companion object {
            val propertiesByName = SortingData::class.declaredMemberProperties
                .associate{
                    @Suppress("UNCHECKED_CAST") // safe because they're all numbers
                    it.name to it as KProperty1<SortingData, Comparable<*>>
                }
        }
    }
    

    Then use the comparator builder functions to construct your builder:

    fun sortedListByAdditionalTags(list: List<Store>, selectedTags: List<SortTag>): List<Store> {
    
        // properties where "isAscending" actually means descending (big numbers first)
        val invertedSortProperties = listOf(
            SortingData::bestMatch,
            SortingData::newest,
            SortingData::ratingAverage,
            SortingData::popularity
        )
        var comparator: Comparator<Store> = compareBy { 0 } // no-op default
        for (sortTag in selectedTags) {
            val property = SortingData.propertiesByName[sortTag.key] ?: continue
            val ascending = if (property in SortingData.invertedSortProperties) !sortTag.isAscending else sortTag.isAscending
            val function: (Store) -> Comparable<*> = { property.invoke(it.sortingData) }
            comparator = if (ascending) comparator.thenBy(function) else comparator.thenByDescending(function)
        }
        return list.sortedWith(comparator)
    }