mydomain
No ADS
No ADS

The idea of designing filter models in FlutterArtist

  1. FilterModel
  2. TildeFilterCriterion
  3. registerFilterModelStructure()
  4. Summary of JSON Layering Benefits
In FlutterArtist, FilterModel is a class that allows you to define criteria and conditional structures for data filtering. The data for these criteria is also stored and managed within the FilterModel, which can be displayed on the UI via the FilterPanel.
Essentially, FilterModel is designed generically to create complex filter models with multiple nested conditions through a Field-Based JSON structure.
Field-Based JSON
{
    "connector": "AND",
    "conditions": [
        {
            "connector": "OR",
            "conditions": [
                {
                    "field": "searchText",
                    "value": "vinfast",
                    "operator": "containsIgnoreCase"
                },
                {
                    "field": "searchText",
                    "value": "honda",
                    "operator": "containsIgnoreCase"
                },
            ]
        },  
        {
            "field": "price",
            "value": 100,
            "operator": "greaterThan"
        },
        {
            "field": "price",
            "value": 200,
            "operator": "lessThan"
        }
    ]
}

1. FilterModel

First, let's examine the definition of the FilterModel class, an abstract class with two Generics parameters.
abstract class FilterModel<
    FILTER_INPUT extends FilterInput, 
    FILTER_CRITERIA extends FilterCriteria
    > 
FILTER_INPUT
An input that provides hints to automatically specify values for criteria instead of user manual operations on the FilterPanel.
An example using FilterInput:
  • FlutterArtist FilterInput - Ví dụ 1
FILTER_CRITERIA
FilterCriteria is a smaller concept compared to FilterModel; it holds the values entered or selected by the user for each criterion. FilterCriteria can export a Field-Based JSON with a conditional structure to be sent to the server for data filtering.
Field-Based JSON
{
    "connector": "AND",
    "conditions": [  
        {
            "field": "albumId",
            "operator": "equalTo",
            "value": 1
        },
        {
            "field": "searchText",
            "operator": "containsIgnoreCase",
            "value": "love"
        } 
    ]
}
In FlutterArtist, the aforementioned JSON is also used to check whether the filter criteria have changed after user actions on the FilterPanel.
No ADS

2. TildeFilterCriterion

"Tilde Criterion" is a special criterion in FlutterArtist. This term was introduced to solve technical issues related to the filter model and may be non-existent or unfamiliar compared to popular libraries.
To understand "Tilde Criterion", consider a scenario: You want to filter products within a price range. On the UI, you design two input fields for MIN and MAX values. These two fields share the same prefix "price" and have the suffixes "~min" and "~max" respectively.
Consequently, the Tilde-Based JSON accurately describes what the user sees on the UI because each TildeFilterCriterion has a 1-1 correspondence with an input field.
Tilde-Based JSON
{
    "connector": "AND",
    "conditions": [
        {
            "tildeCriterionName": "category~",
            "value": "CategoryInfo(2, Electric Motorbike)",
            "operator": "equalTo"
        },
        {
            "tildeCriterionName": "price~min",
            "value": 1000,
            "operator": "greaterThan"
        },
        {
            "tildeCriterionName": "price~max",
            "value": 20000,
            "operator": "lessThan"
        }
    ]
}
No ADS
Criteria-Based JSON is a simplified conditional structure of Tilde-Based JSON where tildeCriterionName(s) are replaced by criterionName(s), while the logical values remain unchanged.
Criteria-Based JSON
{
    "connector": "AND",
    "conditions": [
        {
            "criterionName": "category",
            "value": "CategoryInfo(2, Electric Motorbike)",
            "operator": "equalTo"
        },
        {
            "criterionName": "price",
            "value": 1000,
            "operator": "greaterThan"
        },
        {
            "criterionName": "price",
            "value": 20000,
            "operator": "lessThan"
        }
    ]
}
In FlutterArtist, each FilterCriterion can have one or more TildeFilterCriterion(s). TildeFilterCriterion(s) belonging to the same FilterCriterion share the same name prefix.
Finally, to send data to the server, we need to "flatten" the JSON by converting complex data types (such as CategoryInfo) into simple values (int, double, bool, String) via the toFieldValue() method in MultiOptFilterCriterionDef.
Field-Based JSON
{
    "connector": "AND",
    "conditions": [
        {
            "field": "categoryId",
            "operator": "equalTo",
            "value": 2
        },
        {
            "field": "price",
            "operator": "greaterThan",
            "value": 1000
        },
        {
            "field": "price",
            "operator": "lessThan",
            "value": 20000
        }
    ]
}

3. registerFilterModelStructure()

defineFilterModelStructure() is a crucial abstract method of FilterModel, allowing you to define criteria, their data types, and the condition structure.
FilterModelStructure defineFilterModelStructure();
First, let's look at the filter interface mentioned above.
Define the filter structure using the defineFilterModelStructure() method:
defineFilterModelStructure() (Demo77a)
@override
FilterModelStructure defineFilterModelStructure() {
  return FilterModelStructure(
    criteriaStructure: FilterCriteriaStructure(
      simpleCriterionDefs: [
        SimpleFilterCriterionDef<double>(criterionBaseName: "price"),
      ],
      multiOptCriterionDefs: [
        // Multi Options Single Selection Criterion.
        MultiOptFilterCriterionDef<CategoryInfo>.singleSelection(
          criterionBaseName: "category",
          fieldName: 'categoryId',
          toFieldValue: (CategoryInfo? rawValue) {
            return SimpleVal.ofInt(rawValue?.id);
          },
        ),
      ],
    ),
    conditionStructure: FilterConditionStructure(
      connector: FilterConnector.and,
      conditionDefs: [
        FilterConditionDef.simple(
          tildeCriterionName: "category~",
          operator: FilterOperator.equalTo,
        ),
        FilterConditionDef.simple(
          tildeCriterionName: "price~min",
          operator: FilterOperator.greaterThan,
        ),
        FilterConditionDef.simple(
          tildeCriterionName: "price~max",
          operator: FilterOperator.lessThan,
        ),
      ],
    ),
  );
}
Using the toFieldValue() function to convert complex data types to simple ones is the "secret" of this design. It ensures that the FilterModel can work with high-level objects (ensuring Type Safety), while the server only receives primitive data types to optimize performance.
No ADS
toFieldValue()
MultiOptFilterCriterionDef<CategoryInfo>.singleSelection(
  criterionBaseName: "category",
  fieldName: 'categoryId',
  toFieldValue: (CategoryInfo? rawValue) {
    return SimpleVal.ofInt(rawValue?.id);
  },
),
Next, let's examine a more complex structure, illustrating how FlutterArtist handles nested condition groups.
In this example, we use FilterConditionDef.group to create an "OR" logic for two different Tilde Criterion(s) that both belong to the same root Criterion - album.
defineFilterModelStructure() (Demo56a)
@override
FilterModelStructure defineFilterModelStructure() {
  return FilterModelStructure(
    criteriaStructure: FilterCriteriaStructure(
      simpleCriterionDefs: [
        SimpleFilterCriterionDef<String>(criterionBaseName: "searchText"),
      ],
      multiOptCriterionDefs: [
        // Multi Options Single Selection Criterion.
        MultiOptFilterCriterionDef<AlbumInfo>.singleSelection(
          criterionBaseName: "album",
          fieldName: 'albumId',
          toFieldValue: (AlbumInfo? rawValue) {
            return SimpleVal.ofInt(rawValue?.id);
          },
        ),
      ],
    ),
    conditionStructure: FilterConditionStructure(
      connector: FilterConnector.and,
      conditionDefs: [
        FilterConditionDef.simple(
          tildeCriterionName: "searchText~",
          operator: FilterOperator.containsIgnoreCase,
        ),
        FilterConditionDef.group(
          groupName: "G2",
          connector: FilterConnector.or,
          conditionDefs: [
            FilterConditionDef.simple(
              tildeCriterionName: "album~1",
              operator: FilterOperator.equalTo,
            ),
            FilterConditionDef.simple(
              tildeCriterionName: "album~2",
              operator: FilterOperator.equalTo,
            ),
          ],
        ),
      ],
    ),
  );
}
The resulting output across the JSON transformation layers is as follows:
Reflects the G2 group structure with distinct identifying tildeCriterionName(s).
Tilde-Based JSON
{
    "connector": "AND",
    "conditions": [
        {
            "tildeCriterionName": "searchText~",
            "value": "love",
            "operator": "containsIgnoreCase"
        },
        {
            "connector": "OR",
            "conditions": [
                {
                    "tildeCriterionName": "album~1",
                    "value": "AlbumInfo(1, Favorite English Songs)",
                    "operator": "equalTo"
                },
                {
                    "tildeCriterionName": "album~2",
                    "value": "AlbumInfo(2, Uncategorized)",
                    "operator": "equalTo"
                }
            ]
        }
    ]
}
All tildeCriterionName(s) are standardized to the same criterionName, album.
Criteria-Based JSON
{
    "connector": "AND",
    "conditions": [
        {
            "criterionName": "searchText",
            "value": "love",
            "operator": "containsIgnoreCase"
        },
        {
            "connector": "OR",
            "conditions": [
                {
                    "criterionName": "album",
                    "value": "AlbumInfo(1, Favorite English Songs)",
                    "operator": "equalTo"
                },
                {
                    "criterionName": "album",
                    "value": "AlbumInfo(2, Uncategorized)",
                    "operator": "equalTo"
                }
            ]
        }
    ]
}
The final result with data flattened into albumId and primitive values (int), ready for SQL WHERE clauses or API queries.
Field-Based JSON
{
    "connector": "AND",
    "conditions": [
        {
            "field": "searchText",
            "operator": "containsIgnoreCase",
            "value": "love"
        },
        {
            "connector": "OR",
            "conditions": [
                {
                    "field": "albumId",
                    "operator": "equalTo",
                    "value": 1
                },
                {
                    "field": "albumId",
                    "operator": "equalTo",
                    "value": 2
                }
            ]
        }
    ]
}
No ADS
If you want more advanced examples of FilterModelStructure, please read the articles and examples below:
  • Flutter FilterModelStructure (***)
  • FlutterArtist FilterModelStructure ex2 (***)
Debug Filter Model Viewer
The Debug Filter Model Viewer is a tool that allows you to examine the state and data of a FilterModel. This tool can be opened via a button on the FilterControlBar or through code.
  • FlutterArtist Debug Filter Model Viewer

4. Summary of JSON Layering Benefits

Layered design, progressing from Tilde-Based to Criteria-Based and finally Field-Based, offers three core benefits for large-scale application development:
Decoupling UI and Logic
Developers can modify the UI (adding/removing ~ input fields) without changing the Database or API structure. Tilde-Based JSON acts as the perfect buffer zone.
Type Safety
The entire processing on Mobile occurs with high-level objects (e.g., AlbumInfo). Flattening data via toFieldValue() only happens at the final step, minimizing errors from incorrect formatting.
Backend Query Optimization
The server only receives Field-Based JSON with primitive types and standard operators. This allows the Backend to easily convert JSON into SQL commands or NoSQL queries without additional complex logic.
No ADS
No ADS