Skip to content

Add support for PredefinedFilters for QueryChannels#6415

Draft
VelikovPetar wants to merge 16 commits intodevelopfrom
feature/AND-1162_predefined_filters
Draft

Add support for PredefinedFilters for QueryChannels#6415
VelikovPetar wants to merge 16 commits intodevelopfrom
feature/AND-1162_predefined_filters

Conversation

@VelikovPetar
Copy link
Copy Markdown
Contributor

@VelikovPetar VelikovPetar commented May 6, 2026

Goal

Add support for server-side PredefinedFilters to QueryChannels. Apps can now run a channel query by referencing a filter/sort registered on the backend (by name, plus interpolation values) instead of building the FilterObject/QuerySorter on the client. This lets product/backend tweak filtering rules without an app release.

You can check / update / create the predefined filters for the app "Stream Mobile SDK / Stream SDK - Android" - in the "Predefined Filters" section in the Beta Dashboard

Implementation

Public API

  • QueryChannelsRequest gains predefinedFilter: String?, filterValues: Map<String, Any>?, sortValues: Map<String, Any>?. When predefinedFilter is set, the client-side filter/querySort are ignored by the backend.
  • QueryChannelsResult is a new wrapper around ChatApi.queryChannels that exposes both channels and the backend-resolved PredefinedFilter (parsed name + FilterObject + QuerySorter<Channel>).
  • ChannelListViewModel (Compose) and ChannelListViewModel (UI Components) get a new constructor / ChannelListViewModelFactory overload taking predefinedFilterName, filterValues, sortValues. In predefined mode, setFilters/setQuerySort are no-ops; channel search still narrows the displayed list.

State / offline architecture

  • New QueryChannelsIdentifier sealed type (Standard / Predefined) is the canonical key used by LogicRegistry, StateRegistry, the listener, and the offline repository. A QueryChannelsRequest/QueryChannelsSpec resolves to one or the other depending on whether predefinedFilter is present.
  • QueryChannelsSpec keeps its existing (filter, querySort) constructor (so JVM signatures stay stable) and adds predefinedFilterName/predefinedFilterValues/predefinedSortValues as vars. For predefined queries, those three form the row's stable identity; filter/querySort carry the currently resolved values.
  • QueryChannelsMutableState starts predefined queries with placeholder filter/sort and applies the resolved spec via applyResolvedSpec(...) once the backend response arrives (or the row is rehydrated from disk).
  • Internal ChannelListViewModel.QueryMode (Standard vs Predefined) drives the two flows — pagination, refresh, search, and filter-flow wiring branch on it.

Persistence

  • QueryChannelsEntity gains predefinedFilterName/predefinedFilterValues/predefinedSortValues. QueryChannelsRepository.selectBy(identifier) replaces the previous filter-based lookup.
  • Room DB version bumped to 104 (destructive migration, in line with the rest of the SDK).

Wire format

  • FilterDomainMapping now supports a full round-trip (DTO ↔ domain) so the resolved predefined filter coming back from the server can be parsed into a FilterObject and stored.

Testing

  • Unit tests added/updated across the new boundaries: QueryChannelsIdentifier derivation, MoshiChatApi, FilterDomainMapping (round trip), QuerySortByField round trip, DatabaseQueryChannelsRepository, QueryChannelsLogic / QueryChannelsDatabaseLogic / QueryChannelsListenerState, QueryChannelsMutableState, LogicRegistry, and both ChannelListViewModels.
  • Both sample apps (Compose and UI Components) are wired to a backend-registered android_sample_filter so the feature can be exercised end-to-end on device.

@VelikovPetar VelikovPetar added the pr:new-feature New feature label May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled (or ignored for dependabot PRs).

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

DB Entities have been updated. Do we need to upgrade DB Version?
Modified Entities :

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/queryChannels/internal/QueryChannelsEntity.kt

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.82 MB 5.83 MB 0.01 MB 🟢
stream-chat-android-ui-components 11.02 MB 11.03 MB 0.01 MB 🟢
stream-chat-android-compose 12.37 MB 12.39 MB 0.02 MB 🟢

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 6, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
76.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

return Filters.and(*filters.toTypedArray())
}

@Suppress("SpreadOperator")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe at some point we should consider introducing Filters overloads that take a collection, so we avoid these suppression and the intermediate array (since now we do collection -> array -> set). Not blocking at all for this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would eventually love to migrate to the type-safe filters from core - they would also make this parser much simpler (or not simpler, but rather safer)

* compatibility, we replace the held [_querySpec] instance instead of mutating it in place.
* `cids` and the predefined identity fields are carried over from the previous instance.
*/
fun applyResolvedSpec(filter: FilterObject, sort: QuerySorter<Channel>) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the docs we're saying that this is no op in the standard case, but that responsibility is currently on the callers. I'm a bit concerned that we might be accidentally call this for standard filters in the future.

Some ideas:

  • Maybe we could we use a type specific to predefined filters here to make this un-callable for standard filters? -> although we don't yet have that type as far as I can see
  • We could change the function name and update the documentation to say that this should only be called for predefined filters rather than implying that the function itself is no op
  • We cold make it no op by adding an early return on identifier !is QueryChannelsIdentifier.Predefined

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm good point, I actually re-did this part couple of times, so the docs are a bit outdated. I will revisit this, taking your feedback into consideration.

Comment on lines +39 to +41
val predefinedFilterName: String? = null,
val predefinedFilterValues: Map<String, Any>? = null,
val predefinedSortValues: Map<String, Any>? = null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok that we define these as nullable but then they'll become empty maps when deserialized? Do you think it's worth to align the types/defaults?

Copy link
Copy Markdown
Contributor Author

@VelikovPetar VelikovPetar May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aham I missed this part. Hmm, I will need to go over the flow - I think it shouldn't be an issue, but I will double check.

Comment on lines +26 to +29
* Note on shape: the predefined-filter fields ([predefinedFilterName],
* [predefinedFilterValues], [predefinedSortValues]) are declared as body-level `var` properties
* rather than primary-constructor parameters specifically to keep the primary `<init>(filter,
* querySort)` and the synthesized `copy(filter, querySort)` JVM signatures unchanged.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we instead introduce a secondary constructor + a copy function? Something like:

public data class QueryChannelsSpec(
    val filter: FilterObject,
    val querySort: QuerySorter<Channel>,
    val cids: Set<String> = emptySet(),
    val predefinedFilterName: String? = null,
    val predefinedFilterValues: Map<String, Any>? = null,
    val predefinedSortValues: Map<String, Any>? = null,
) {
    public constructor(
        filter: FilterObject,
        querySort: QuerySorter<Channel>,
    ) : this(filter, querySort, emptySet(), null, null, null)

    public fun copy(
        filter: FilterObject = this.filter,
        querySort: QuerySorter<Channel> = this.querySort,
    ): QueryChannelsSpec = QueryChannelsSpec(
        filter = filter,
        querySort = querySort,
        cids = cids,
        predefinedFilterName = predefinedFilterName,
        predefinedFilterValues = predefinedFilterValues,
        predefinedSortValues = predefinedSortValues,
    )
}

So we avoid the vars and we make sure equals and hashCode take all properties into account (at the moment, a predefined filter is treated as equal to a standard filter that has the same resolved filter/sort).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will look into it!


public final class io/getstream/chat/android/compose/viewmodel/channels/ChannelListViewModelFactory : androidx/lifecycle/ViewModelProvider$Factory {
public static final field $stable I
public fun <init> ()V
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok that we lost this? I believe it only affects java callers, but would break them if they just call new ChannelListViewModelFactory()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm good point, let me see what we can do about this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:new-feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants