├── LICENSE ├── test └── multiselect_form_field_test.dart ├── demo.gif ├── lib ├── multiselect_form_field.dart └── src │ ├── multi_select_form_field_item.dart │ ├── multi_select_tag.dart │ ├── multi_select_form_field_list_item.dart │ └── multi_select_form_field.dart ├── .metadata ├── CHANGELOG.md ├── README.md ├── pubspec.yaml ├── .gitignore └── pubspec.lock /LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /test/multiselect_form_field_test.dart: -------------------------------------------------------------------------------- 1 | void main() { 2 | } 3 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevenosse/multi_select_form_field/HEAD/demo.gif -------------------------------------------------------------------------------- /lib/multiselect_form_field.dart: -------------------------------------------------------------------------------- 1 | library multiselect_form_field; 2 | 3 | export './src/multi_select_form_field.dart'; 4 | export './src/multi_select_form_field_list_item.dart'; 5 | export './src/multi_select_form_field_item.dart'; -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8af6b2f038c1172e61d418869363a28dffec3cb4 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - Initial release 2 | 3 | * Initial functionnalities 4 | 5 | ## [0.0.2] - Bug fixes and plugin enhancement 6 | 7 | * Replace the element list item from `Map` to `MultiSelectFormFieldItem` 8 | * Made the selected element list scrollable (instead of letting it expand on bottom) 9 | * Fixed the item selection bug 10 | * Improved plugin customisation 11 | * Other bug fixes and core improvements 12 | -------------------------------------------------------------------------------- /lib/src/multi_select_form_field_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MultiSelectFormFieldItem { 4 | /// The text to display on the list element 5 | /// 6 | /// Can't be [Null] 7 | String label; 8 | 9 | /// The value of the item, intended to be exploited later 10 | /// 11 | /// It's of type [dynamic], override it with the type you need. 12 | dynamic value; 13 | 14 | /// a boolean determining wether the element is selected or not 15 | /// 16 | /// default to [false] 17 | bool isSelected; 18 | 19 | /// The style to apply to the label of the list 20 | /// 21 | /// It's of type [TextStyle] 22 | final TextStyle labelStyle; 23 | 24 | final Widget leading; 25 | final Widget trailing; 26 | 27 | MultiSelectFormFieldItem({ 28 | this.isSelected = false, 29 | this.value, 30 | this.leading, 31 | this.trailing, 32 | this.label, 33 | this.labelStyle = const TextStyle(fontSize: 14.0), 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/multi_select_tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MultiSelectTag extends StatelessWidget { 4 | final String label; 5 | final VoidCallback onRemove; 6 | final Color tagColor; 7 | 8 | MultiSelectTag({this.label, this.tagColor, @required this.onRemove}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Padding( 13 | padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), 14 | child: Container( 15 | decoration: BoxDecoration( 16 | color: tagColor ?? Colors.lightBlue, 17 | borderRadius: BorderRadius.circular(4.0), 18 | ), 19 | child: Padding( 20 | padding: const EdgeInsets.all(8.0), 21 | child: Row( 22 | mainAxisSize: MainAxisSize.min, 23 | children: [ 24 | Text( 25 | "$label", 26 | style: TextStyle( 27 | fontSize: 12.0, 28 | fontWeight: FontWeight.w600, 29 | ), 30 | ), 31 | SizedBox(width: 10), 32 | GestureDetector( 33 | onTap: onRemove, 34 | child: Icon( 35 | Icons.close, 36 | color: Colors.black26, 37 | size: 15, 38 | ), 39 | ), 40 | ], 41 | ), 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/multi_select_form_field_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MultiSelectFieldListItem extends StatelessWidget { 4 | /// The label of the item 5 | /// 6 | /// Takes a String as parameter 7 | final String label; 8 | 9 | /// Defines whether an item is selected or not 10 | /// 11 | /// The default value is [false] 12 | final bool selected; 13 | 14 | /// The leading widget 15 | /// 16 | /// If null, nothing will be displayed 17 | final Widget leading; 18 | 19 | /// The method called when an unselected item is tapped 20 | /// The element is of type [VoidCallback] 21 | final VoidCallback onSelected; 22 | 23 | /// The style to apply on each label element 24 | final TextStyle labelStyle; 25 | 26 | MultiSelectFieldListItem({ 27 | @required this.label, 28 | this.selected = false, 29 | @required this.onSelected, 30 | this.leading, 31 | this.labelStyle, 32 | }) : assert(leading != null && label != null); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Padding( 37 | padding: const EdgeInsets.symmetric(vertical: 6.0), 38 | child: Row( 39 | children: [ 40 | if(leading != null) CircleAvatar(), 41 | SizedBox(width: 10), 42 | Text( 43 | "$label", 44 | style: labelStyle ?? TextStyle( 45 | fontWeight: FontWeight.w600, 46 | color: selected ? Colors.black38 : Colors.black, 47 | ), 48 | ), 49 | ], 50 | ), 51 | ); 52 | } 53 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi Select Form Field 2 | 3 | A dropdown button Widget allowing one to choose multiple elements. 4 | 5 | ## Install 6 | 7 | Add these line under your dependencies section: 8 | 9 | ```yaml 10 | multiselect_form_field: 11 | git: 12 | url: https://github.com/stevenosse/multi_select_form_field.git 13 | ref: master 14 | ``` 15 | 16 | ## Demo 17 | 18 | ![alt text](./demo.gif "Demo gif") 19 | 20 | ### Features 21 | - Regular widget 22 | - Very simple to implement 23 | - Can retrieve the list of selected elements 24 | - Build custom list elements 25 | - Can retrieve the list of unselected elements 26 | 27 | ### Example 28 | 29 | ```dart 30 | MultiSelectFormField( 31 | key: _multiSelectKey, 32 | tagColor: Colors.blue, 33 | elementList: List.generate( 34 | 15, 35 | (index) => MultiSelectFormFieldItem( 36 | labelStyle: TextStyle(fontWeight: FontWeight.w600), 37 | leading: CircleAvatar(), 38 | label: "Test $index", 39 | value: "test", 40 | isSelected: index.isEven, 41 | ), 42 | ), 43 | ), 44 | ``` 45 | 46 | 47 | ## Retrieve more parameters 48 | 49 | Give a Key to the widget, declared like : 50 | ```dart 51 | final GlobalKey _multiSelectKey = GlobalKey();` 52 | 53 | ``` 54 | 55 | And then : 56 | ```dart 57 | var selectedElements = _multiSelectKey.currentState.selectedElements; // Retrieve all the selected elements 58 | var unselectedElements = _multiSelectKey.currentState.unselectedElements; // Retrieve all the unselected elements 59 | ``` 60 | ## Contribute 61 | Every contributions are welcomed 62 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: multiselect_form_field 2 | description: A flutter dropdown button Widget allowing one to choose multiple elements. 3 | version: 0.0.2 4 | author: 5 | homepage: 6 | 7 | environment: 8 | sdk: ">=2.7.0 <3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | 24 | # To add assets to your package, add an assets section, like this: 25 | # assets: 26 | # - images/a_dot_burr.jpeg 27 | # - images/a_dot_ham.jpeg 28 | # 29 | # For details regarding assets in packages, see 30 | # https://flutter.dev/assets-and-images/#from-packages 31 | # 32 | # An image asset can refer to one or more resolution-specific "variants", see 33 | # https://flutter.dev/assets-and-images/#resolution-aware. 34 | 35 | # To add custom fonts to your package, add a fonts section here, 36 | # in this "flutter" section. Each entry in this list should have a 37 | # "family" key with the font family name, and a "fonts" key with a 38 | # list giving the asset and other descriptors for the font. For 39 | # example: 40 | # fonts: 41 | # - family: Schyler 42 | # fonts: 43 | # - asset: fonts/Schyler-Regular.ttf 44 | # - asset: fonts/Schyler-Italic.ttf 45 | # style: italic 46 | # - family: Trajan Pro 47 | # fonts: 48 | # - asset: fonts/TrajanPro.ttf 49 | # - asset: fonts/TrajanPro_Bold.ttf 50 | # weight: 700 51 | # 52 | # For details regarding fonts in packages, see 53 | # https://flutter.dev/custom-fonts/#from-packages 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.0.13" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.6.0" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.4.1" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.0.0" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.3" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.14.12" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.1.4" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | image: 71 | dependency: transitive 72 | description: 73 | name: image 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "2.1.12" 77 | matcher: 78 | dependency: transitive 79 | description: 80 | name: matcher 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "0.12.6" 84 | meta: 85 | dependency: transitive 86 | description: 87 | name: meta 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.1.8" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.6.4" 98 | petitparser: 99 | dependency: transitive 100 | description: 101 | name: petitparser 102 | url: "https://pub.dartlang.org" 103 | source: hosted 104 | version: "2.4.0" 105 | quiver: 106 | dependency: transitive 107 | description: 108 | name: quiver 109 | url: "https://pub.dartlang.org" 110 | source: hosted 111 | version: "2.1.3" 112 | sky_engine: 113 | dependency: transitive 114 | description: flutter 115 | source: sdk 116 | version: "0.0.99" 117 | source_span: 118 | dependency: transitive 119 | description: 120 | name: source_span 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.7.0" 124 | stack_trace: 125 | dependency: transitive 126 | description: 127 | name: stack_trace 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.9.3" 131 | stream_channel: 132 | dependency: transitive 133 | description: 134 | name: stream_channel 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "2.0.0" 138 | string_scanner: 139 | dependency: transitive 140 | description: 141 | name: string_scanner 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "1.0.5" 145 | term_glyph: 146 | dependency: transitive 147 | description: 148 | name: term_glyph 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "1.1.0" 152 | test_api: 153 | dependency: transitive 154 | description: 155 | name: test_api 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "0.2.15" 159 | typed_data: 160 | dependency: transitive 161 | description: 162 | name: typed_data 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "1.1.6" 166 | vector_math: 167 | dependency: transitive 168 | description: 169 | name: vector_math 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "2.0.8" 173 | xml: 174 | dependency: transitive 175 | description: 176 | name: xml 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "3.6.1" 180 | sdks: 181 | dart: ">=2.7.0 <3.0.0" 182 | -------------------------------------------------------------------------------- /lib/src/multi_select_form_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:multiselect_form_field/src/multi_select_form_field_list_item.dart'; 3 | import 'package:multiselect_form_field/src/multi_select_form_field_item.dart'; 4 | import 'package:multiselect_form_field/src/multi_select_tag.dart'; 5 | 6 | class MultiSelectFormField extends StatefulWidget { 7 | final Key key; 8 | 9 | /// The list of elements to display 10 | /// 11 | final List elementList; 12 | 13 | /// The color of the displayed tag color 14 | /// 15 | /// The default color is [Colors.lightBlue] 16 | final Color tagColor; 17 | 18 | /// The text to display when no element is selected 19 | /// 20 | /// The default text is ["No item selected yet"] 21 | final String emptyLabel; 22 | 23 | MultiSelectFormField({ 24 | this.key, 25 | @required this.elementList, 26 | this.emptyLabel, 27 | this.tagColor, 28 | }) : super(key: key); 29 | 30 | @override 31 | MultiSelectFormFieldState createState() => MultiSelectFormFieldState(); 32 | } 33 | 34 | class MultiSelectFormFieldState extends State { 35 | /// Retrieve the list of selected elements 36 | get selectedElements => 37 | widget.elementList.where((e) => e.isSelected == true).toList(); 38 | 39 | /// Retrieve the list of unselected elements 40 | get unselectedElements => 41 | widget.elementList.where((e) => e.isSelected == false).toList(); 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | final Size size = MediaQuery.of(context).size; 46 | return Container( 47 | width: size.width, 48 | height: size.height * 0.3, 49 | child: Stack( 50 | children: [ 51 | Container( 52 | width: size.width, 53 | decoration: BoxDecoration( 54 | color: Colors.white, 55 | borderRadius: BorderRadius.circular(6.0), 56 | boxShadow: [ 57 | BoxShadow( 58 | blurRadius: 4.0, 59 | color: Colors.black.withOpacity(0.15), 60 | ), 61 | ], 62 | ), 63 | child: Padding( 64 | padding: EdgeInsets.only(top: 55.0, left: 12.0, right: 12.0), 65 | child: ListView.builder( 66 | itemCount: widget.elementList.length, 67 | itemBuilder: (BuildContext context, int index) { 68 | final element = widget.elementList[index]; 69 | return Material( 70 | color: Colors.transparent, 71 | child: InkWell( 72 | onTap: () { 73 | setState(() { 74 | widget.elementList[index].isSelected = true; 75 | }); 76 | }, 77 | child: Padding( 78 | padding: const EdgeInsets.all(8.0), 79 | child: Row( 80 | children: [ 81 | if (element.leading != null) ...[ 82 | element.leading, 83 | SizedBox(width: 5) 84 | ], 85 | if (element.label != null) 86 | Expanded( 87 | child: Text( 88 | element.label, 89 | style: element.labelStyle.apply( 90 | color: element.isSelected 91 | ? Colors.black45 92 | : element.labelStyle.color, 93 | ), 94 | ), 95 | ), 96 | if (element.trailing != null) ...[ 97 | SizedBox(width: 5), 98 | element.trailing, 99 | ], 100 | ], 101 | ), 102 | ), 103 | ), 104 | ); 105 | }, 106 | ), 107 | ), 108 | ), 109 | _buildSelectedList(), 110 | ], 111 | ), 112 | ); 113 | } 114 | 115 | _buildSelectedList() { 116 | final Size size = MediaQuery.of(context).size; 117 | return Container( 118 | width: size.width, 119 | decoration: BoxDecoration( 120 | color: Colors.white, 121 | borderRadius: BorderRadius.circular(4.0), 122 | boxShadow: [ 123 | BoxShadow( 124 | blurRadius: 2.0, 125 | color: Colors.black.withOpacity(0.1), 126 | ), 127 | ], 128 | ), 129 | child: Padding( 130 | padding: EdgeInsets.all(4.0), 131 | child: SingleChildScrollView( 132 | scrollDirection: Axis.horizontal, 133 | child: Row( 134 | children: [ 135 | ..._buildSelectedElementList(), 136 | ], 137 | ), 138 | ), 139 | ), 140 | ); 141 | } 142 | 143 | _buildSelectedElementList() { 144 | final elementList = 145 | widget.elementList.where((e) => e.isSelected == true).toList(); 146 | if (elementList.isEmpty) return [_buildEmptyLabel()]; 147 | return elementList 148 | .map( 149 | (e) => MultiSelectTag( 150 | label: e.label, 151 | tagColor: widget.tagColor, 152 | onRemove: () { 153 | widget.elementList.where((elt) => elt == e).first.isSelected = 154 | false; 155 | setState(() {}); 156 | }, 157 | ), 158 | ) 159 | .toList(); 160 | } 161 | 162 | _buildEmptyLabel() { 163 | return Container( 164 | child: Padding( 165 | padding: const EdgeInsets.all(8.0), 166 | child: Text( 167 | widget.emptyLabel ?? "No item selected yet", 168 | style: TextStyle(fontSize: 12.0, color: Colors.black38), 169 | ), 170 | ), 171 | ); 172 | } 173 | } 174 | --------------------------------------------------------------------------------