mydomain
No ADS
No ADS

FlutterArtist Filter Example

  1. Structure of the example
  2. Song02aShelf
  3. Song02aFilterModel
  4. Song02aFilterCriteria
  5. Song02aBlock
  6. Song02aFilterPanel
Filtering is a fundamental feature in any application, and FlutterArtist is no exception. In this article, we will implement a simple example: displaying a list of songs and filtering them by two criteria: "Song Title" and "Album".
Through this example, you will master the core concepts directly related to filtering:
  • FilterModel
  • FilterPanel
  • FilterCriteria
This article is based on the Dem02a example, with the full source code available in the FlutterArtist Demo project, which you can download at the link below:
  • Download FlutterArtist Demo
Before starting with this example, you should read the article below to better understand the concept of FilterModel in FlutterArtist and its design philosophy.

1. Structure of the example

Before we begin coding, we need to look at the overall structure of this example, which will be explained in detail in the following sections.
No ADS
The filter in this example allows users to search for songs by title and album.

2. Song02aShelf

Song02aShelf has a straightforward structure, consisting of a FilterModel and a Block. FilterModel(s) are declared independently from Block(s) because a single FilterModel can be shared across multiple Block(s) or Scalar(s).
song02a_shelf.dart
class Song02aShelf extends Shelf {
  @override
  ShelfStructure defineShelfStructure() {
    return ShelfStructure(
      filterModels: {
        Song02aFilterModel.filterName: Song02aFilterModel(), //
      },
      blocks: [
        Song02aBlock(
          name: Song02aBlock.blkName,
          description: null,
          config: BlockConfig(),
          filterModelName: Song02aFilterModel.filterName,
          formModel: null,
          childBlocks: [],
        ),
      ],
    );
  }

  Song02aBlock findSong02aBlock() {
    return findBlock(Song02aBlock.blkName) as Song02aBlock;
  }

  Song02aFilterModel findSong02aFilterModel() {
    return findFilterModel(Song02aFilterModel.filterName) as Song02aFilterModel;
  }
}

3. Song02aFilterModel

In FlutterArtist, FilterModel is the data model that manages the data and state of criteria. FilterPanel is the UI that displays this model, allowing users to enter or select criteria. All UI interactions are synchronized back into the FilterModel.
TildeCriterion (or TildeFilterCriterion) is a term introduced to address issues related to filter modeling in FlutterArtist, and it is explained in the article below:
defineFilterModelStructure()
You must implement this method to define the filter structure.
song02a_filter_model.dart (*)
class Song02aFilterModel
    extends
        FilterModel<
          EmptyFilterInput, //
          Song02aFilterCriteria
        > {
  static const String filterName = "song02a-filter-model";

  @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.simple(
            tildeCriterionName: "album~",
            operator: FilterOperator.equalTo,
          ),
        ],
      ),
    );
  }
  ...
}
No ADS
SimpleFilterCriterionDef
Define a simple criterion, a blueprint for creating simple TildeCriterion(s). A simple TildeCriterion corresponds one-to-one to a simple input field on the FilterPanel such as FormBuilderTextField, FormBuilderCheckbox...
MultiOptFilterCriterionDef
Define a criterion with multiple options that the user can select one or more of. A TildeCriterion created from this design corresponds one-to-one to a multi-option input field on the FilterPanel, such as:
  • FormBuilderDropdown, FormBuilderMultiDropdown, FormBuilderCheckboxGroup,..
These types of criteria can be independent of each other or have a parent-child relationship.
See an example of a FilterModel with parent-child criteria:
  • FlutterArtist FilterModel MultiOptFilterCriterion - Ví dụ 1
Based on the conditional structure you define, the TildeFilterCriterion(s) will be created, each TildeFilterCriterion corresponding one-to-one to an input field on the FilterPanel.
FilterCriteriaStructure
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);
      },
    ),
  ],
)
You can view the values of the TildeFilterCriterion(s) in real time on the Debug Filter Model Viewer.
  • FlutterArtist Debug Filter Model Viewer
FilterModel.performLoadMultiOptTildeCriterionXData()
The performLoadMultiOptTildeCriterionXData() method is used to load data for the MultiOptTildeFilterCriterion(s).
No ADS
performLoadMultiOptTildeCriterionXData()
@override
Future<XData?> performLoadMultiOptTildeCriterionXData({
  required String multiOptTildeCriterionName,
  required String multiOptCriterionBaseName,
  required Object? parentMultiOptTildeCriterionValue,
  required SelectionType selectionType,
  required EmptyFilterInput? filterInput,
}) async {
  if (multiOptTildeCriterionName == "album~") {
    ApiResult<AlbumInfoPage> result = await _albumRestProvider.allAlbums();
    // Throw ApiError:
    result.throwIfError();
    // IMPORTANT: Generics should be declared explicitly.
    return ListXData<int, AlbumInfo>.fromPageData(
      pageData: result.data,
      getItemId: (item) => item.id,
    );
  }
  return null;
}
  • FlutterArtist ApiResult
  • FlutterArtist XData
FilterModel.specifyDefaultValueForMultiOptTildeCriterion()
This method is used to specify a default value for MultiOptTildeFilterCriterion after its data has been successfully loaded. Note: This method is used one or more times throughout the lifecycle of the FilterModel (depending on the configuration).
  • Flutter FilterModelStructure (***)
specifyDefaultValueForMultiOptTildeCriterion()
@override
OptValueWrap? specifyDefaultValueForMultiOptTildeCriterion({
  required String multiOptTildeCriterionName,
  required String multiOptCriterionBaseName,
  required Object? parentMultiOptTildeCriterionValue,
  required SelectionType selectionType,
  required XData multiOptTildeCriterionXData,
}) {
  if (multiOptTildeCriterionName == "album~") {
    var listXData = multiOptTildeCriterionXData as ListXData<int, AlbumInfo>;
    AlbumInfo? album = listXData.data.firstOrNull;
    return OptValueWrap.single(album);
  }
  return null;
}
FilterModel.createNewFilterCriteria()
The createNewFilterCriteria() method is called automatically to create a FilterCriteria object. Here, we extract values from tildeCriteriaMap and assign them to FilterCriteria properties.
No ADS
FilterModel stores the values of the TildeCriteria(s) in a Map<String,dynamic> object, which is passed as a parameter to the createNewFilterCriteria() method.
createNewFilterCriteria()
@override
Song02aFilterCriteria createNewFilterCriteria({
  required Map<String, dynamic> tildeCriteriaMap,
}) {
  return Song02aFilterCriteria(
    album: tildeCriteriaMap["album~"],  
    searchText: tildeCriteriaMap["searchText~"],  
  );
}
The FilterCriteria object obtained from the previous step will be passed as a parameter to the Block.performQuery() or Scalar.performQuery() method.
// Block:
Future<ApiResult<PageData<ITEM>?>> performQuery({
  required Object? parentBlockCurrentItem,
  required FILTER_CRITERIA filterCriteria,
  required SortableCriteria sortableCriteria,
  required Pageable pageable,
});

// Scalar:
Future<ApiResult<VALUE>> performQuery({
  required Object? parentScalarValue,
  required FILTER_CRITERIA filterCriteria,
});
  • FlutterArtist Debug Filter Model Viewer

4. Song02aFilterCriteria

Essentially, from the FilterCriteria object you will get the fieldBasedJSON and can send it to the server for querying and filtering data.
Block.performQuery()
@override
Future<ApiResult<PageData<SongInfo>?>> performQuery({
  required Object? parentBlockCurrentItem,
  required Song02aFilterCriteria filterCriteria,
  required SortableCriteria sortableCriteria,
  required Pageable pageable,
}) async { 
  return await songRestProvider.someAdvQuery(
    pageable: pageable,
    fieldBasedJSON: filterCriteria.fieldBasedJSON, 
  );
}
No ADS
However, for simple applications, sending a fieldBasedJSON might complicate the server-side logic. Instead, you can opt for a more straightforward solution by passing criterion values directly into the FilterCriteria object.
Song02aFilterCriteria
class Song02aFilterCriteria extends FilterCriteria {
  final String? searchText;
  final AlbumInfo? album;

  Song02aFilterCriteria({required this.album, required this.searchText});
  ..
}  
Song02aBlock.performQuery()
@override
Future<ApiResult<PageData<SongInfo>?>> performQuery({
  required Object? parentBlockCurrentItem,
  required Song02aFilterCriteria filterCriteria,
  required SortableCriteria sortableCriteria,
  required Pageable pageable,
}) async {
  return await songRestProvider.querySearch(
    pageable: pageable,
    searchText: filterCriteria.searchText,
    albumId: filterCriteria.album?.id,
  );
}
...
Filter Criteria Viewer
The Debug Filter Criteria Viewer is a tool that shows you the latest FilterCriteria object extracted from the FilterModel and used to query the Block most recently. This tool can be accessed via a BlockControlBar button or code.
  • FlutterArtist Debug Filter Criteria Viewer

5. Song02aBlock

song02a_block.dart

class Song02aBlock
    extends
        Block<
          int, //
          SongInfo,
          SongData,
          EmptyFilterInput,
          Song02aFilterCriteria,
          EmptyFormInput,
          EmptyAdditionalFormRelatedData
        > {
  static const String blkName = "song02a-block";

  final songRestProvider = SongRestProvider();

  Song02aBlock({
    required super.name,
    required super.description,
    required super.config,
    required super.filterModelName,
    required super.formModel,
    required super.childBlocks,
  });

  @override
  Future<ApiResult<PageData<SongInfo>?>> performQuery({
    required Object? parentBlockCurrentItem,
    required Song02aFilterCriteria filterCriteria,
    required SortableCriteria sortableCriteria,
    required Pageable pageable,
  }) async {
    return await songRestProvider.querySearch(
      pageable: pageable,
      searchText: filterCriteria.searchText,
      albumId: filterCriteria.album?.id,
    );
  }
  ...
}
Let's look at how to create FilterCriteria and how it's used in the Block.performQuery() method.
Song02aFilterModel
Song02aBlock
@override
Song02aFilterCriteria createNewFilterCriteria({
  required Map<String, dynamic> tildeCriteriaMap,
}) {
  return Song02aFilterCriteria(
    album: tildeCriteriaMap["album~"],  
    searchText: tildeCriteriaMap["searchText~"],  
  );
}
@override
Future<ApiResult<PageData<SongInfo>?>> performQuery({
  required Object? parentBlockCurrentItem,
  required Song02aFilterCriteria filterCriteria,
  required SortableCriteria sortableCriteria,
  required Pageable pageable,
}) async {
  return await songRestProvider.querySearch(
    pageable: pageable,
    searchText: filterCriteria.searchText,
    albumId: filterCriteria.album?.id,
  );
}

6. Song02aFilterPanel

No ADS
Song02aFilterPanel is a class extended from FilterPanel, used to render the filter data model onto the user interface.
Important Note: Instead of creating a separate FilterPanel class, you can use FilterPanelBuilder directly. However, this is not recommended as it becomes harder to maintain as the project grows.
song02a_filter_panel.dart
class Song02aFilterPanel extends FilterPanel<Song02aFilterModel> {
  const Song02aFilterPanel({
    super.key,
    required super.filterModel,
  });

  @override
  Widget buildContent(BuildContext context) {
    return CustomAppContainer(
      padding: EdgeInsets.all(5),
      child: Column(
        children: [
          buildFilterBar(context),
          SizedBox(height: 5),
          FaFormBuilderTextField.topLabel(
            name: "searchText",
            labelText: "Search Text",
            maxLines: 1,
            onChanged: (_) {
              _querySong02a();
            },
          ),
          SizedBox(height: 5),
          FaFormBuilderDeselectableDropdown<AlbumInfo>.topLabel(
            name: 'album',
            labelText: "Album",
            items: filterModel.getMultiOptCriterionData("album") ?? [],
            getItemText: (AlbumInfo item) {
              return item.name;
            },
            onChanged: (_) {
              _querySong02a();
            },
          ),
          SizedBox(height: 5),
          TextButton(
            onPressed: _querySong02a,
            child: Text("Search"),
          ),
        ],
      ),
    );
  }

  Future<void> _querySong02a() async {
    FlutterArtist.codeFlowLogger.addMethodCall(
      ownerClassInstance: this,
      currentStackTrace: StackTrace.current,
      parameters: null,
    );
    // Query all blocks and scalars associated with this filterModel.
    await filterModel.queryAll();
  }
}
FlutterArtist uses the fluttter_fa_form_builder library package with widgets like FaFormBuilderTextField, FaFormBuilderDropdown, etc., replacing TextField, Dropdown, etc., to help you build and handle form logic more simply.
The most important parameter of a FaFormBuilderXxx utility in FilterPanel is "name", which is mapped one-to-one with a TildeFilterCriterion of the same name as the FilterModel. Data synchronization between FilterPanel and FilterModel is completely automatic and transparent by the library.
  • FlutterArtist Fa FormBuilder
No ADS
No ADS