Form

Multi Select

A multi select displays a list of drop-down options for the user to pick from. It is a form-field and can therefore be used in a form.

For single selections, consider using a select.

For touch devices, a select tile group or select menu tile is generally recommended over this.

1const fruits = [
2 'Apple',
3 'Banana',
4 'Blueberry',
5 'Grapes',
6 'Lemon',
7 'Mango',
8 'Kiwi',
9 'Orange',
10 'Peach',
11 'Pear',
12 'Pineapple',
13 'Plum',
14 'Raspberry',
15 'Strawberry',
16 'Watermelon',
17];
18
19class MultiSelectExample extends StatelessWidget {
20 @override
21 Widget build(BuildContext _) => FMultiSelect<String>.rich(
22 hint: const Text('Select a fruit'),
23 format: Text.new,
24 children: [
25 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),
26 ],
27 );
28}
29

CLI

To generate and customize this style:

dart run forui style create multi-select

Usage

FMultiSelect(...)

1FMultiSelect<String>(
2 style: const .delta(emptyTextStyle: .delta()),
3 enabled: true,
4 items: const {'Apple': 'apple', 'Banana': 'banana', 'Cherry': 'cherry'},
5)

FMultiSelect.rich(...)

1FMultiSelect<String>.rich(
2 style: const .delta(emptyTextStyle: .delta()),
3 enabled: true,
4 format: Text.new,
5 children: [
6 .item(title: const Text('Apple'), value: 'apple'),
7 .item(title: const Text('Banana'), value: 'banana'),
8 .section(
9 label: const Text('More'),
10 items: {'Cherry': 'cherry', 'Date': 'date'},
11 ),
12 ],
13)

FMultiSelect.search(...)

1FMultiSelect<String>.search(
2 const {'Apple': 'apple', 'Banana': 'banana', 'Cherry': 'cherry'},
3 style: const .delta(emptyTextStyle: .delta()),
4 enabled: true,
5 filter: (query) =>
6 ['apple', 'banana', 'cherry'].where((e) => e.startsWith(query)),
7)

FMultiSelect.searchBuilder(...)

1FMultiSelect<String>.searchBuilder(
2 style: const .delta(emptyTextStyle: .delta()),
3 enabled: true,
4 filter: (query) =>
5 ['apple', 'banana', 'cherry'].where((e) => e.startsWith(query)),
6 format: Text.new,
7 contentBuilder: (context, style, values) => [
8 for (final value in values) .item(title: Text(value), value: value),
9 ],
10)

Examples

Detailed

1@override
2Widget build(BuildContext _) => FMultiSelect<String>.rich(
3 hint: const Text('Type'),
4 format: Text.new,
5 children: [
6 .item(
7 prefix: const Icon(FIcons.bug),
8 title: const Text('Bug'),
9 subtitle: const Text('An unexpected problem or behavior'),
10 value: 'Bug',
11 ),
12 .item(
13 prefix: const Icon(FIcons.filePlusCorner),
14 title: const Text('Feature'),
15 subtitle: const Text('A new feature or enhancement'),
16 value: 'Feature',
17 ),
18 .item(
19 prefix: const Icon(FIcons.messageCircleQuestionMark),
20 title: const Text('Question'),
21 subtitle: const Text('A question or clarification'),
22 value: 'Question',
23 ),
24 ],
25);
26

Sections

1@override
2Widget build(BuildContext _) => FMultiSelect<String>.rich(
3 hint: const Text('Select a timezone'),
4 format: Text.new,
5 children: [
6 .section(
7 label: const Text('North America'),
8 items: {
9 for (final item in [
10 'Eastern Standard Time (EST)',
11 'Central Standard Time (CST)',
12 'Mountain Standard Time (MST)',
13 'Pacific Standard Time (PST)',
14 'Alaska Standard Time (AKST)',
15 'Hawaii Standard Time (HST)',
16 ])
17 item: item,
18 },
19 ),
20 .section(
21 label: const Text('South America'),
22 items: {
23 for (final item in [
24 'Argentina Time (ART)',
25 'Bolivia Time (BOT)',
26 'Brasilia Time (BRT)',
27 'Chile Standard Time (CLT)',
28 ])
29 item: item,
30 },
31 ),
32 .section(
33 label: const Text('Europe & Africa'),
34 items: {
35 for (final item in [
36 'Greenwich Mean Time (GMT)',
37 'Central European Time (CET)',
38 'Eastern European Time (EET)',
39 'Western European Summer Time (WEST)',
40 'Central Africa Time (CAT)',
41 'Eastern Africa Time (EAT)',
42 ])
43 item: item,
44 },
45 ),
46 .section(
47 label: const Text('Asia'),
48 items: {
49 for (final item in [
50 'Moscow Time (MSK)',
51 'India Standard Time (IST)',
52 'China Standard Time (CST)',
53 'Japan Standard Time (JST)',
54 'Korea Standard Time (KST)',
55 'Indonesia Standard Time (IST)',
56 ])
57 item: item,
58 },
59 ),
60 .section(
61 label: const Text('Australia & Pacific'),
62 items: {
63 for (final item in [
64 'Australian Western Standard Time (AWST)',
65 'Australian Central Standard Time (ACST)',
66 'Australian Eastern Standard Time (AEST)',
67 'New Zealand Standard Time (NZST)',
68 'Fiji Time (FJT)',
69 ])
70 item: item,
71 },
72 ),
73 ],
74);
75

Dividers

1@override
2Widget build(BuildContext _) => FMultiSelect<String>.rich(
3 hint: const Text('Select a level'),
4 contentDivider: .full,
5 format: Text.new,
6 children: [
7 .section(
8 label: const Text('Level 1'),
9 divider: .indented,
10 items: {
11 for (final item in ['A', 'B']) item: '1$item',
12 },
13 ),
14 .section(
15 label: const Text('Level 2'),
16 items: {
17 for (final item in ['A', 'B']) item: '2$item',
18 },
19 ),
20 .item(title: const Text('Level 3'), value: '3'),
21 .item(title: const Text('Level 4'), value: '4'),
22 ],
23);
24

Searchable

Sync

1const fruits = [
2 'Apple',
3 'Banana',
4 'Blueberry',
5 'Grapes',
6 'Lemon',
7 'Mango',
8 'Kiwi',
9 'Orange',
10 'Peach',
11 'Pear',
12 'Pineapple',
13 'Plum',
14 'Raspberry',
15 'Strawberry',
16 'Watermelon',
17];
18
19class SyncMultiSelectExample extends StatelessWidget {
20 @override
21 Widget build(BuildContext _) => FMultiSelect<String>.searchBuilder(
22 hint: const Text('Select a fruit'),
23 format: Text.new,
24 filter: (query) => query.isEmpty
25 ? fruits
26 : fruits.where((f) => f.toLowerCase().startsWith(query.toLowerCase())),
27 contentBuilder: (context, _, fruits) => [
28 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),
29 ],
30 );
31}
32

Async

1const fruits = [
2 'Apple',
3 'Banana',
4 'Blueberry',
5 'Grapes',
6 'Lemon',
7 'Mango',
8 'Kiwi',
9 'Orange',
10 'Peach',
11 'Pear',
12 'Pineapple',
13 'Plum',
14 'Raspberry',
15 'Strawberry',
16 'Watermelon',
17];
18
19class AsyncMultiSelectExample extends StatelessWidget {
20 @override
21 Widget build(BuildContext _) => FMultiSelect<String>.searchBuilder(
22 hint: const Text('Select a fruit'),
23 format: Text.new,
24 filter: (query) async {
25 await Future.delayed(const Duration(seconds: 1));
26 return query.isEmpty
27 ? fruits
28 : fruits.where(
29 (fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()),
30 );
31 },
32 contentBuilder: (context, _, fruits) => [
33 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),
34 ],
35 );
36}
37

Async with Custom Loading

1const fruits = [
2 'Apple',
3 'Banana',
4 'Blueberry',
5 'Grapes',
6 'Lemon',
7 'Mango',
8 'Kiwi',
9 'Orange',
10 'Peach',
11 'Pear',
12 'Pineapple',
13 'Plum',
14 'Raspberry',
15 'Strawberry',
16 'Watermelon',
17];
18
19class AsyncLoadingMultiSelectExample extends StatelessWidget {
20 @override
21 Widget build(BuildContext _) => FMultiSelect<String>.searchBuilder(
22 hint: const Text('Select a fruit'),
23 format: Text.new,
24 filter: (query) async {
25 await Future.delayed(const Duration(seconds: 1));
26 return query.isEmpty
27 ? fruits
28 : fruits.where(
29 (fruit) => fruit.toLowerCase().startsWith(query.toLowerCase()),
30 );
31 },
32 contentLoadingBuilder: (context, style) => Padding(
33 padding: const .all(8.0),
34 child: Text(
35 'Here be dragons...',
36 style: style.fieldStyle.contentTextStyle.resolve({}),
37 ),
38 ),
39 contentBuilder: (context, _, fruits) => [
40 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),
41 ],
42 );
43}
44

Async with Custom Error Handling

1const fruits = [
2 'Apple',
3 'Banana',
4 'Blueberry',
5 'Grapes',
6 'Lemon',
7 'Mango',
8 'Kiwi',
9 'Orange',
10 'Peach',
11 'Pear',
12 'Pineapple',
13 'Plum',
14 'Raspberry',
15 'Strawberry',
16 'Watermelon',
17];
18
19class AsyncErrorMultiSelectExample extends StatelessWidget {
20 @override
21 Widget build(BuildContext _) => FMultiSelect<String>.searchBuilder(
22 hint: const Text('Select a fruit'),
23 format: Text.new,
24 filter: (query) async {
25 await Future.delayed(const Duration(seconds: 1));
26 throw StateError('Error loading data');
27 },
28 contentBuilder: (context, _, fruits) => [
29 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),
30 ],
31 contentErrorBuilder: (context, error, trace) {
32 final style = context.theme.selectStyle.fieldStyle.iconStyle.resolve({});
33 return Padding(
34 padding: const .all(8.0),
35 child: Icon(
36 FIcons.messageCircleX,
37 size: style.size,
38 color: style.color,
39 ),
40 );
41 },
42 );
43}
44

Behavior

Clearable

1const fruits = [
2 'Apple',
3 'Banana',
4 'Blueberry',
5 'Grapes',
6 'Lemon',
7 'Mango',
8 'Kiwi',
9 'Orange',
10 'Peach',
11 'Pear',
12 'Pineapple',
13 'Plum',
14 'Raspberry',
15 'Strawberry',
16 'Watermelon',
17];
18
19class ClearableMultiSelectExample extends StatelessWidget {
20 @override
21 Widget build(BuildContext _) => FMultiSelect<String>.rich(
22 hint: const Text('Select a fruit'),
23 format: Text.new,
24 clearable: true,
25 children: [
26 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),
27 ],
28 );
29}
30

Custom Formatting

1@override
2Widget build(BuildContext _) =>
3 FMultiSelect<({String firstName, String lastName})>.rich(
4 hint: const Text('Select a user'),
5 format: (user) => Text('${user.firstName} ${user.lastName}'),
6 children: [
7 for (final user in users)
8 .item(title: Text(user.firstName), value: user),
9 ],
10 );
11

Min & Max

1const fruits = [
2 'Apple',
3 'Banana',
4 'Blueberry',
5 'Grapes',
6 'Lemon',
7 'Mango',
8 'Kiwi',
9 'Orange',
10 'Peach',
11 'Pear',
12 'Pineapple',
13 'Plum',
14 'Raspberry',
15 'Strawberry',
16 'Watermelon',
17];
18
19class MinMaxMultiSelectExample extends StatelessWidget {
20 @override
21 Widget build(BuildContext context) => FMultiSelect<String>.rich(
22 control: const .managed(min: 1, max: 3),
23 hint: const Text('Select favorite fruits'),
24 format: Text.new,
25 children: [
26 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),
27 ],
28 );
29}
30

Sorted

1const fruits = [
2 'Apple',
3 'Banana',
4 'Blueberry',
5 'Grapes',
6 'Lemon',
7 'Mango',
8 'Kiwi',
9 'Orange',
10 'Peach',
11 'Pear',
12 'Pineapple',
13 'Plum',
14 'Raspberry',
15 'Strawberry',
16 'Watermelon',
17];
18
19class SortedMultiSelectExample extends StatelessWidget {
20 @override
21 Widget build(BuildContext _) => FMultiSelect<String>.rich(
22 hint: const Text('Select favorite fruits'),
23 format: Text.new,
24 sort: (a, b) => a.compareTo(b),
25 children: [
26 for (final fruit in fruits) .item(title: Text(fruit), value: fruit),
27 ],
28 );
29}
30

Form

1class FormMultiSelectExample extends StatefulWidget {
2 @override
3 State<FormMultiSelectExample> createState() => _FormMultiSelectExampleState();
4}
5
6class _FormMultiSelectExampleState extends State<FormMultiSelectExample> {
7 final _key = GlobalKey<FormState>();
8
9 @override
10 Widget build(BuildContext context) => Form(
11 key: _key,
12 child: Column(
13 crossAxisAlignment: .start,
14 spacing: 25,
15 children: [
16 FMultiSelect<String>.rich(
17 label: const Text('Department'),
18 description: const Text('Choose your dream department(s)'),
19 hint: const Text('Select departments'),
20 format: Text.new,
21 validator: (departments) =>
22 departments.isEmpty ? 'Please select departments' : null,
23 children: [
24 for (final department in const [
25 'Engineering',
26 'Marketing',
27 'Sales',
28 'Human Resources',
29 'Finance',
30 ])
31 .item(title: Text(department), value: department),
32 ],
33 ),
34 FButton(
35 child: const Text('Submit'),
36 onPress: () {
37 if (_key.currentState!.validate()) {
38 // Form is valid, do something with departments
39 }
40 },
41 ),
42 ],
43 ),
44 );
45}
46

On this page