FlutterArtist Filter FormBuilderMultiDropDown Ex1
In this article, we will explore how to configure an advanced filter with parent-child criteria. The highlight of this example is that the child criterion, "departments" allows Multi-selection, providing a flexible and powerful filtering experience.

Our discussion focuses on the "Demo20a" example in the FlutterArtist Demo. Here, Employee20aFilterModel sets up three search criteria: Employee Name, Company, and Departments. Notably, Company and Departments are bound by a parent-child relationship: once a Company is selected, the corresponding Department list reloads to ensure data integrity.
Note: If you are new to FlutterArtist Filter, please look at the example below first instead of this one.
- Download FlutterArtist Demo
2. Employee20aShelf
The structure of the Shelf in this example is very simple, consisting only of a FilterModel and a Block.
employee20a_shelf.dart
class Employee20aShelf extends Shelf {
@override
ShelfStructure defineShelfStructure() {
return ShelfStructure(
filterModels: {
Employee20aFilterModel.filterName: Employee20aFilterModel(),
},
blocks: [
Employee20aBlock(
name: Employee20aBlock.blkName,
description: null,
config: BlockConfig(),
filterModelName: Employee20aFilterModel.filterName,
formModel: null,
childBlocks: [],
),
],
);
}
Employee20aFilterModel findEmployee20aFilterModel() {
return findFilterModel(Employee20aFilterModel.filterName)
as Employee20aFilterModel;
}
Employee20aBlock findEmployee20aBlock() {
return findBlock(Employee20aBlock.blkName) as Employee20aBlock;
}
}3. Employee20aFilterModel
This is the most critical part of the article. Pay close attention to how we define the "company" (Parent) and "departments" (Child) criteria.
Employee20aFilterModel.defineFilterModelStructure()
@override
FilterModelStructure defineFilterModelStructure() {
return FilterModelStructure(
simpleCriterionDefs: [
SimpleCriterionDef<String>(criterionBaseName: "searchText"),
],
multiOptCriterionDefs: [
MultiOptCriterionDef<CompanyInfo>.singleSelection(
criterionBaseName: "company",
fieldName: 'companyId',
toFieldValue: (CompanyInfo? rawValue) {
return SimpleVal.ofInt(rawValue?.id);
},
children: [
// Multi Option Multi Selection Criterion.
MultiOptCriterionDef<DepartmentInfo>.multiSelection(
criterionBaseName: "departments",
fieldName: 'departmentId',
toFieldValue: (DepartmentInfo? rawValue) {
return SimpleVal.ofInt(rawValue?.id);
},
),
],
),
],
conditionConnector: ConditionConnector.and,
conditionDefs: [
ConditionDef.condition(
tildeCriterionName: "searchText~",
operator: FilterOperator.containsIgnoreCase,
),
ConditionDef.condition(
tildeCriterionName: "company~",
operator: FilterOperator.equalTo,
),
ConditionDef.condition(
tildeCriterionName: "departments~",
operator: FilterOperator.inCollection,
),
],
);
}Let's look at how to define the criterion "departments":

MultiOptCriterionDef<DepartmentInfo>.multiSelection
MultiOptFilterCriterionDef<DepartmentInfo>.multiSelection(
criterionBaseName: "departments",
fieldName: 'departmentId',
toFieldValue: (DepartmentInfo? rawValue) {
return SimpleVal.ofInt(rawValue?.id);
},
),Note on the difference between singleSelection and multiSelection:In the FlutterArtist system, MultiOptFilterCriterionDef.multiSelection() is specifically designed to handle multi-item picklists. However, there is a crucial rule to remember: unlike singleSelection (which can act as a "Parent" and contain nested children), multiSelection is always treated as a "Leaf" criterion. This means it CANNOT contain any further child criteria beneath it.
No ADS
FilterModel.performLoadMultiOptTildeCriterionXData()
The performLoadMultiOptTildeCriterionXData() method is invoked sequentially for each MultiOptTildeFilterCriterion. The library follows a golden rule: Root criteria data is always loaded first, followed by Child criteria, and finally the Leaf criteria.
performLoadMultiOptTildeCriterionXData()
@override
Future<XData?> performLoadMultiOptTildeCriterionXData({
required String multiOptTildeCriterionName,
required String multiOptCriterionBaseName,
required Object? parentMultiOptTildeCriterionValue,
required SelectionType selectionType,
required EmptyFilterInput? filterInput,
}) async {
if (multiOptTildeCriterionName == "company~") {
ApiResult<CompanyInfoPage> result = await _companyProvider.queryAll();
// Throw ApiError
result.throwIfError();
//
return ListXData<int, CompanyInfo>.fromPageData(
pageData: result.data,
getItemId: (item) => item.id,
);
} else if (multiOptTildeCriterionName == "departments~") {
//
// Because "departments" is child of "company".
// So [parentMultiOptTildeCriterionValue] value is definitely non-null,
// this guaranteed by the library.
//
CompanyInfo company = parentMultiOptTildeCriterionValue as CompanyInfo;
ApiResult<DepartmentInfoPage> result = await _departmentRestProvider
.queryAllByCompanyId(companyId: company.id);
result.throwIfError();
//
return ListXData<int, DepartmentInfo>.fromPageData(
pageData: result.data,
getItemId: (item) => item.id,
);
}
return null;
}
- FlutterArtist Debug Filter Model Viewer
- FlutterArtist ApiResult
- FlutterArtist XData
FilterModel.specifyDefaultValueForMultiOptTildeCriterion()
This method is called to specify a default value for each MultiOptTildeFilterCriterion.
specifyDefaultValueForMultiOptTildeCriterion()
@override
OptValueWrap? specifyDefaultValueForMultiOptTildeCriterion({
required String multiOptTildeCriterionName,
required String multiOptCriterionBaseName,
required Object? parentMultiOptTildeCriterionValue,
required SelectionType selectionType,
required XData multiOptTildeCriterionXData,
}) {
if (multiOptTildeCriterionName == "company~") {
var listXData =
multiOptTildeCriterionXData as ListXData<int, CompanyInfo>;
List<CompanyInfo> list = listXData.data;
if (list.isNotEmpty) {
return OptValueWrap.single(list.first);
}
return null;
} else if (multiOptTildeCriterionName == "departments~") {
CompanyInfo company = parentMultiOptTildeCriterionValue as CompanyInfo;
var listXData =
multiOptTildeCriterionXData as ListXData<int, DepartmentInfo>;
List<DepartmentInfo> departments = listXData.data;
// Select all department in MultiDropDown.
return OptValueWrap.multi(departments);
}
return null;
}FilterModel.createNewFilterCriteria()

createNewFilterCriteria()
@override
Employee20aFilterCriteria createNewFilterCriteria({
required Map<String, dynamic> tildeCriteriaMap,
}) {
return Employee20aFilterCriteria(
searchText: tildeCriteriaMap["searchText~"],
company: tildeCriteriaMap["company~"],
departments: tildeCriteriaMap["departments~"],
);
}No ADS
4. Employee20aFilterCriteria
employee20a_filter_criteria.dart
class Employee20aFilterCriteria extends FilterCriteria {
final String? searchText;
final CompanyInfo? company;
final List<DepartmentInfo>? departments;
const Employee20aFilterCriteria({
required this.searchText,
required this.company,
required this.departments,
});
}
- FlutterArtist Debug Filter Criteria Viewer
5. Employee20aBlock
Finally, in the Block.performQuery() method, we use the filterCriteria object to call the API. Notice how departmentIdsAsString is extracted from the list of selected departments to be sent to the Server.
employee20a_block.dart (*)
class Employee20aBlock
extends
Block<
int, //
EmployeeInfo,
EmployeeData,
EmptyFilterInput,
Employee20aFilterCriteria,
EmptyFormInput,
EmptyAdditionalFormRelatedData
> {
static const blkName = "employee20a-block";
final employeeRestProvider = EmployeeRestProvider();
Employee20aBlock({
required super.name,
required super.description,
required super.config,
required super.filterModelName,
required super.formModel,
required super.childBlocks,
});
@override
Future<ApiResult<PageData<EmployeeInfo>?>> performQuery({
required Object? parentBlockCurrentItem,
required Employee20aFilterCriteria filterCriteria,
required SortableCriteria sortableCriteria,
required Pageable pageable,
}) async {
return await employeeRestProvider.queryAdvanced(
pageable: pageable,
searchText: filterCriteria.searchText,
companyId: filterCriteria.company?.id,
departmentIdsAsString: filterCriteria.departmentIdsAsString,
);
}
...
}
- FlutterArtist FilterCriteria
No ADS
6. Employee20aFilterPanel
The Employee20aFilterPanel class creates the user interface for the filter.

employee20a_filter_panel.dart
class Employee20aFilterPanel extends FilterPanel<Employee20aFilterModel> {
const Employee20aFilterPanel({required super.filterModel, super.key});
@override
Widget buildContent(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildFilterBar(context),
const Divider(),
FaFormBuilderTextField.topLabel(
name: "searchText~",
labelText: "Search Text",
maxLines: 1,
onChanged: _onSearchTextChanged,
),
const SizedBox(height: 10),
FaFormBuilderDeselectableDropdown<CompanyInfo>.topLabel(
key: const Key("filter-company"),
name: "company~",
labelText: "Company",
// List<CompanyInfo>:
items: filterModel.getMultiOptTildeCriterionData("company~") ?? [],
getItemText: (item) => item.name,
onChanged: _onSelectCompany,
),
const SizedBox(height: 10),
FaFormBuilderMultiDropdown<int, DepartmentInfo>.topLabel(
key: const Key("filter-department"),
name: "departments~",
dropdownTitle: 'Select departments',
labelText: "Department",
// List<DepartmentInfo>:
items:
filterModel.getMultiOptTildeCriterionData("departments~") ?? [],
getItemText: (item) => item.name,
onChanged: _onChangeDepartment,
),
],
);
}
Future<void> _onSearchTextChanged(String? text) async {
FlutterArtist.codeFlowLogger.addMethodCall(
ownerClassInstance: this,
currentStackTrace: StackTrace.current,
parameters: {},
);
//
await filterModel.queryAll();
}
Future<void> _onSelectCompany(CompanyInfo? company) async {
FlutterArtist.codeFlowLogger.addMethodCall(
ownerClassInstance: this,
currentStackTrace: StackTrace.current,
parameters: {"company": company},
);
//
await filterModel.queryAll();
}
Future<void> _onChangeDepartment(List<DepartmentInfo>? departments) async {
FlutterArtist.codeFlowLogger.addMethodCall(
ownerClassInstance: this,
currentStackTrace: StackTrace.current,
parameters: {"departments": departments},
);
//
await filterModel.queryAll();
}
}No ADS
FlutterArtist
- Basic concepts in Flutter Artist
- FlutterArtist Block ex1
- FlutterArtist Filter Example
- FlutterArtist FilterModel MultiOptFilterCriterion ex1
- FlutterArtist FilterInput Example 1
- FlutterArtist Form ex1
- The idea of designing filter models in FlutterArtist
- FlutterArtist FormModel.patchFormFields() Ex1
- FlutterArtist BlockQuickItemUpdateAction Example
- FlutterArtist BlockNumberPagination Ex1
- FlutterArtist GridView Infinite Scroll Example
- FlutterArtist BlockQuickMultiItemCreationAction Example
- FlutterArtist ListView Infinite Scroll Pagination Example
- FlutterArtist Pagination
- FlutterArtist Sort DropdownSortPanel Example
- FlutterArtist Dio
- FlutterArtist BlockBackendAction Example
- FlutterArtist BackgroundWebDownloadAction Example
- FlutterArtist Block External Shelf Event ex1
- FlutterArtist Filter FormBuilderMultiDropDown Ex1
- FlutterArtist Master-detail Blocks ex1
- FlutterArtist Scalar ex1
- FlutterArtist Pagination Davi table Infinite Scroll Ex1
- FlutterArtist Filter Tree FormBuilderField ex1
- FlutterArtist Filter FormBuilderRadioGroup ex1
- FlutterArtist Form Parent-child MultiOptFormProp ex1
- FlutterArtist Manual Sorting ReorderableGridView Example
- FlutterArtist Manual Sorting ReorderableListView
- FlutterArtist Scalar External Shelf Event ex1
- FlutterArtist Code Flow Viewer
- FlutterArtist Log Viewer
- FlutterArtist config
- FlutterArtist StorageStructure
- FlutterArtist Debug Storage Viewer
- FlutterArtist DebugMenu
- FlutterArtist Debug UI Components Viewer
- FlutterArtist Debug Shelf Structure Viewer
- FlutterArtist Context Provider Views
- FlutterArtist FilterModelStructure ex1
- FlutterArtist FilterModelStructure ex2
- FlutterArtist FilterModelStructure ex3
- FlutterArtist Internal Shelf Event ex1
- FlutterArtist Deferring External Shelf Events (Ex1)
- FlutterArtist DropdownSortPanel
Show More

