Don Madrino
Don Madrino

Reputation: 147

Generic collection filtering using predicates

I got a tricky issue concerning collection filtering in kotlin...

I got a base class that manages a list of items and I want to be able to filter the list with a keyword so I extended the class with Filterable methods.

What I want to do is to be able to extend multiple classes with this 'base class' so the filter mecanism is the same for all classes.

These classes don't have the same properties... In one, the filtering must occur depending if the keyword is found in the 'name' while in another class the filtering is done on the 'comment' property.

Here some code:

data class ProductInfo(): {
    var _name: String
    var name: String
            get() = _name
            set(value) { _name = value }
}

abstract class BaseFirestoreAdapter<T : BaseFirestoreAdapter.DataInterface, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>(), Filterable
{

    var sourceList: MutableList<ProductInfo> = ArrayList()

    ...

    override fun performFiltering(keyword: CharSequence): FilterResults {

        val keywordRegex = keyword.toString().toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.LITERAL))

        filteredList = sourceList.filter {
                keywordRegex.containsMatchIn(Normalizer.normalize(it.name, Normalizer.Form.NFD).replace("[^\\p{ASCII}]".toRegex(RegexOption.IGNORE_CASE), ""))
        }

        results.values = filteredList.sortedWith(orderComparator)
        results.count = filteredList.size
    }

    ...
}

I developped the 'base class' so it works with the first class mentionned above (filtering is done with the 'it.name') and it works but now that I'm trying to make it generic (T) to use it with the second class (comments), I can't find a way to do it...

I thought I could pass a class related predicate defining how to match the items during the filtering but since the keyword is only known in the performFiltering method, I can't create properly the predicate outside of this method...

I'm kinda out of ideas now! lol

Any of you have an idea?


UPDATE: Following @Tenfour04's suggestion, I tried adapting it to my code which passes filtering predicates via a method instead of using the constructor but it does not compile unless I replace "ActivyInfo::comments" with something like "ActivyInfo::comments.name" but then the value I get for "searchedProperty(it)" in debug is "name" which is not the comment value.

Here is the code:

CommentAdapter:

override fun getFilter(): Filter {

        super.setFilter(
                { it.state != ProductState.HIDDEN },
                { ActivyInfo::comments },
                compareBy<ProductInfo> { it.state }.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name })

        return super.getFilter()
}

BaseAdapter:

lateinit var defaultFilterPredicate : (T) -> Boolean
lateinit var searchedProperty : (T) -> CharSequence
lateinit var orderComparator : Comparator<T>

fun setFilter(defaultPredicate: (T) -> Boolean, property: (T) -> CharSequence, comparator: Comparator<T> ) {
    defaultFilterPredicate = defaultPredicate
    searchedProperty = property
    orderComparator = comparator
}

override fun performFiltering(constraint: CharSequence): FilterResults {

        ...

        filteredList = sourceList.filter {
                constraintRegex.containsMatchIn(Normalizer.normalize(searchedProperty(it), Normalizer.Form.NFD).replace("[^\\p{ASCII}]".toRegex(RegexOption.IGNORE_CASE), ""))
        }

        ...

    }

Upvotes: 0

Views: 207

Answers (1)

Tenfour04
Tenfour04

Reputation: 93581

You can pass into the constructor a parameter that specifies the property as a function.

abstract class BaseFirestoreAdapter<T : BaseFirestoreAdapter.DataInterface, VH : RecyclerView.ViewHolder>(val filteredProperty: (T) -> CharSequence) : RecyclerView.Adapter<VH>(), Filterable
{

    var sourceList: MutableList<T> = ArrayList()

    // ...

    override fun performFiltering(keyword: CharSequence): FilterResults {

        val keywordRegex = keyword.toString().toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.LITERAL))

        filteredList = sourceList.filter {
                keywordRegex.containsMatchIn(Normalizer.normalize(filteredProperty(it), Normalizer.Form.NFD).replace("[^\\p{ASCII}]".toRegex(RegexOption.IGNORE_CASE), ""))
        }

        results.values = filteredList.sortedWith(orderComparator)
        results.count = filteredList.size
    }

    ...
}

The changes I made to yours were adding the constructor parameter filteredProperty, Changing the sourceList type to T, and replacing it.name with filteredProperty(it).

So subclasses will have to call this super-constructor, passing the property in like this:

data class SomeData(val comments: String)

class SomeDataAdapter: BaseFirestoreAdapter<SomeData>(SomeData::comments) {
    //...
}

Or if you want to keep it generic:

class SomeDataAdapter(filteredProperty: (T) -> CharSequence): BaseFirestoreAdapter<SomeData>(filteredProperty) //...

Upvotes: 1

Related Questions