├── example ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ └── .gitignore ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ ├── manifest.json │ └── index.html ├── android │ ├── gradle.properties │ ├── .gitignore │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ └── values │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── build.gradle ├── README.md ├── .metadata ├── .gitignore ├── test │ └── widget_test.dart ├── pubspec.yaml ├── pubspec.lock └── lib │ └── main.dart ├── analysis_options.yaml ├── lib ├── util │ ├── horizontal_scrollbar.dart │ ├── multi_select_list_type.dart │ ├── multi_select_item.dart │ └── multi_select_actions.dart ├── multi_select_flutter.dart ├── chip_display │ └── multi_select_chip_display.dart ├── dialog │ ├── mult_select_dialog.dart │ └── multi_select_dialog_field.dart ├── bottom_sheet │ ├── multi_select_bottom_sheet.dart │ └── multi_select_bottom_sheet_field.dart └── chip_field │ └── multi_select_chip_field.dart ├── test └── multi_select_flutter_test.dart ├── .metadata ├── LICENSE ├── .gitignore ├── pubspec.yaml ├── pubspec.lock ├── README.md └── CHANGELOG.md /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | Example for multi_select_flutter package. 4 | 5 | Check out some different ways to implement multi_select_flutter. 6 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | exclude: [build/**] 3 | strong-mode: 4 | implicit-casts: true 5 | 6 | linter: 7 | rules: 8 | - camel_case_types -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /lib/util/horizontal_scrollbar.dart: -------------------------------------------------------------------------------- 1 | class HorizontalScrollBar { 2 | final bool isAlwaysShown; 3 | 4 | HorizontalScrollBar({this.isAlwaysShown = false}); 5 | } 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/util/multi_select_list_type.dart: -------------------------------------------------------------------------------- 1 | /// Used by MultiSelectDialog and MultiSelectBottomSheet to determine which type of list to render. 2 | enum MultiSelectListType { LIST, CHIP } 3 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/multi_select_flutter_test.dart: -------------------------------------------------------------------------------- 1 | // import 'package:flutter_test/flutter_test.dart'; 2 | // import 'package:flutter_multi_select/flutter_multi_select.dart'; 3 | 4 | void main() { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pt10/multi_select_flutter/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/util/multi_select_item.dart: -------------------------------------------------------------------------------- 1 | /// A model class used to represent a selectable item. 2 | class MultiSelectItem { 3 | final T value; 4 | final String label; 5 | bool selected = false; 6 | 7 | MultiSelectItem(this.value, this.label); 8 | } 9 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.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: b041144f833e05cf463b8887fa12efdec9493488 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/.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: b041144f833e05cf463b8887fa12efdec9493488 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/multi_select_flutter.dart: -------------------------------------------------------------------------------- 1 | export 'util/multi_select_item.dart'; 2 | export 'util/multi_select_list_type.dart'; 3 | export 'util/multi_select_actions.dart'; 4 | export 'dialog/mult_select_dialog.dart'; 5 | export 'dialog/multi_select_dialog_field.dart'; 6 | export 'bottom_sheet/multi_select_bottom_sheet.dart'; 7 | export 'bottom_sheet/multi_select_bottom_sheet_field.dart'; 8 | export 'chip_display/multi_select_chip_display.dart'; 9 | export 'chip_field/multi_select_chip_field.dart'; 10 | export 'util/horizontal_scrollbar.dart'; 11 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | 7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 8 | def properties = new Properties() 9 | 10 | assert localPropertiesFile.exists() 11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 12 | 13 | def flutterSdkPath = properties.getProperty("flutter.sdk") 14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 16 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/.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 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Exceptions to above rules. 43 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 44 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Chris Botha 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 4 | following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 7 | disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 13 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 14 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 15 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 16 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 17 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 18 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.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.yaml: -------------------------------------------------------------------------------- 1 | name: multi_select_flutter 2 | description: A flexible multi select package for Flutter. Make multi select widgets the way you want. 3 | version: 4.1.3 4 | repository: https://github.com/CHB61/flutter-multi-select 5 | issue_tracker: https://github.com/CHB61/flutter-multi-select 6 | documentation: https://github.com/CHB61/flutter-multi-select 7 | homepage: https://github.com/CHB61/flutter-multi-select 8 | 9 | environment: 10 | sdk: '>=2.12.0 <3.0.0' 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | collection: ^1.15.0 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | 21 | # For information on the generic Dart part of this file, see the 22 | # following page: https://dart.dev/tools/pub/pubspec 23 | 24 | # The following section is specific to Flutter. 25 | flutter: 26 | 27 | # To add assets to your package, add an assets section, like this: 28 | # assets: 29 | # - images/a_dot_burr.jpeg 30 | # - images/a_dot_ham.jpeg 31 | # 32 | # For details regarding assets in packages, see 33 | # https://flutter.dev/assets-and-images/#from-packages 34 | # 35 | # An image asset can refer to one or more resolution-specific "variants", see 36 | # https://flutter.dev/assets-and-images/#resolution-aware. 37 | 38 | # To add custom fonts to your package, add a fonts section here, 39 | # in this "flutter" section. Each entry in this list should have a 40 | # "family" key with the font family name, and a "fonts" key with a 41 | # list giving the asset and other descriptors for the font. For 42 | # example: 43 | # fonts: 44 | # - family: Schyler 45 | # fonts: 46 | # - asset: fonts/Schyler-Regular.ttf 47 | # - asset: fonts/Schyler-Italic.ttf 48 | # style: italic 49 | # - family: Trajan Pro 50 | # fonts: 51 | # - asset: fonts/TrajanPro.ttf 52 | # - asset: fonts/TrajanPro_Bold.ttf 53 | # weight: 700 54 | # 55 | # For details regarding fonts in packages, see 56 | # https://flutter.dev/custom-fonts/#from-packages 57 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.example" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | } 64 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 7 | 8 | version: 1.0.0+1 9 | 10 | environment: 11 | sdk: ">=2.7.0 <3.0.0" 12 | 13 | dependencies: 14 | flutter: 15 | sdk: flutter 16 | multi_select_flutter: 17 | path: ../ 18 | 19 | # The following adds the Cupertino Icons font to your application. 20 | # Use with the CupertinoIcons class for iOS style icons. 21 | cupertino_icons: ^0.1.3 22 | 23 | dev_dependencies: 24 | flutter_test: 25 | sdk: flutter 26 | 27 | # For information on the generic Dart part of this file, see the 28 | # following page: https://dart.dev/tools/pub/pubspec 29 | 30 | # The following section is specific to Flutter. 31 | flutter: 32 | # The following line ensures that the Material Icons font is 33 | # included with your application, so that you can use the icons in 34 | # the material Icons class. 35 | uses-material-design: true 36 | 37 | # To add assets to your application, add an assets section, like this: 38 | # assets: 39 | # - images/a_dot_burr.jpeg 40 | # - images/a_dot_ham.jpeg 41 | 42 | # An image asset can refer to one or more resolution-specific "variants", see 43 | # https://flutter.dev/assets-and-images/#resolution-aware. 44 | 45 | # For details regarding adding assets from package dependencies, see 46 | # https://flutter.dev/assets-and-images/#from-packages 47 | 48 | # To add custom fonts to your application, add a fonts section here, 49 | # in this "flutter" section. Each entry in this list should have a 50 | # "family" key with the font family name, and a "fonts" key with a 51 | # list giving the asset and other descriptors for the font. For 52 | # example: 53 | # fonts: 54 | # - family: Schyler 55 | # fonts: 56 | # - asset: fonts/Schyler-Regular.ttf 57 | # - asset: fonts/Schyler-Italic.ttf 58 | # style: italic 59 | # - family: Trajan Pro 60 | # fonts: 61 | # - asset: fonts/TrajanPro.ttf 62 | # - asset: fonts/TrajanPro_Bold.ttf 63 | # weight: 700 64 | # 65 | # For details regarding fonts from package dependencies, 66 | # see https://flutter.dev/custom-fonts/#from-packages 67 | -------------------------------------------------------------------------------- /lib/util/multi_select_actions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'multi_select_item.dart'; 3 | 4 | /// Contains common actions that are used by different multi select classes. 5 | class MultiSelectActions { 6 | List onItemCheckedChange( 7 | List selectedValues, T itemValue, bool checked) { 8 | if (checked) { 9 | selectedValues.add(itemValue); 10 | } else { 11 | selectedValues.remove(itemValue); 12 | } 13 | return selectedValues; 14 | } 15 | 16 | /// Pops the dialog from the navigation stack and returns the initially selected values. 17 | void onCancelTap(BuildContext ctx, List initiallySelectedValues) { 18 | Navigator.pop(ctx, initiallySelectedValues); 19 | } 20 | 21 | /// Pops the dialog from the navigation stack and returns the selected values. 22 | /// Calls the onConfirm function if one was provided. 23 | void onConfirmTap( 24 | BuildContext ctx, List selectedValues, Function(List)? onConfirm) { 25 | Navigator.pop(ctx, selectedValues); 26 | if (onConfirm != null) { 27 | onConfirm(selectedValues); 28 | } 29 | } 30 | 31 | /// Accepts the search query, and the original list of items. 32 | /// If the search query is valid, return a filtered list, otherwise return the original list. 33 | List> updateSearchQuery( 34 | String? val, List> allItems) { 35 | if (val != null && val.trim().isNotEmpty) { 36 | List> filteredItems = []; 37 | for (var item in allItems) { 38 | if (item.label.toLowerCase().contains(val.toLowerCase())) { 39 | filteredItems.add(item); 40 | } 41 | } 42 | return filteredItems; 43 | } else { 44 | return allItems; 45 | } 46 | } 47 | 48 | /// Toggles the search field. 49 | bool onSearchTap(bool showSearch) { 50 | return !showSearch; 51 | } 52 | 53 | List> separateSelected(List> list) { 54 | List> _selectedItems = []; 55 | List> _nonSelectedItems = []; 56 | 57 | _nonSelectedItems.addAll(list.where((element) => !element.selected)); 58 | _nonSelectedItems.sort((a, b) => a.label.compareTo(b.label)); 59 | _selectedItems.addAll(list.where((element) => element.selected)); 60 | _selectedItems.sort((a, b) => a.label.compareTo(b.label)); 61 | 62 | return [..._selectedItems, ..._nonSelectedItems]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 23 | 27 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: "direct main" 34 | description: 35 | name: collection 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.16.0" 39 | fake_async: 40 | dependency: transitive 41 | description: 42 | name: fake_async 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.3.1" 46 | flutter: 47 | dependency: "direct main" 48 | description: flutter 49 | source: sdk 50 | version: "0.0.0" 51 | flutter_test: 52 | dependency: "direct dev" 53 | description: flutter 54 | source: sdk 55 | version: "0.0.0" 56 | matcher: 57 | dependency: transitive 58 | description: 59 | name: matcher 60 | url: "https://pub.dartlang.org" 61 | source: hosted 62 | version: "0.12.12" 63 | material_color_utilities: 64 | dependency: transitive 65 | description: 66 | name: material_color_utilities 67 | url: "https://pub.dartlang.org" 68 | source: hosted 69 | version: "0.1.5" 70 | meta: 71 | dependency: transitive 72 | description: 73 | name: meta 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "1.8.0" 77 | path: 78 | dependency: transitive 79 | description: 80 | name: path 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.8.2" 84 | sky_engine: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.99" 89 | source_span: 90 | dependency: transitive 91 | description: 92 | name: source_span 93 | url: "https://pub.dartlang.org" 94 | source: hosted 95 | version: "1.9.0" 96 | stack_trace: 97 | dependency: transitive 98 | description: 99 | name: stack_trace 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "1.10.0" 103 | stream_channel: 104 | dependency: transitive 105 | description: 106 | name: stream_channel 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "2.1.0" 110 | string_scanner: 111 | dependency: transitive 112 | description: 113 | name: string_scanner 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.1.1" 117 | term_glyph: 118 | dependency: transitive 119 | description: 120 | name: term_glyph 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.2.1" 124 | test_api: 125 | dependency: transitive 126 | description: 127 | name: test_api 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "0.4.12" 131 | vector_math: 132 | dependency: transitive 133 | description: 134 | name: vector_math 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "2.1.2" 138 | sdks: 139 | dart: ">=2.17.0-0 <3.0.0" 140 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.16.0" 39 | cupertino_icons: 40 | dependency: "direct main" 41 | description: 42 | name: cupertino_icons 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "0.1.3" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.3.1" 53 | flutter: 54 | dependency: "direct main" 55 | description: flutter 56 | source: sdk 57 | version: "0.0.0" 58 | flutter_test: 59 | dependency: "direct dev" 60 | description: flutter 61 | source: sdk 62 | version: "0.0.0" 63 | matcher: 64 | dependency: transitive 65 | description: 66 | name: matcher 67 | url: "https://pub.dartlang.org" 68 | source: hosted 69 | version: "0.12.12" 70 | material_color_utilities: 71 | dependency: transitive 72 | description: 73 | name: material_color_utilities 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.1.5" 77 | meta: 78 | dependency: transitive 79 | description: 80 | name: meta 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.8.0" 84 | multi_select_flutter: 85 | dependency: "direct main" 86 | description: 87 | path: ".." 88 | relative: true 89 | source: path 90 | version: "4.1.3" 91 | path: 92 | dependency: transitive 93 | description: 94 | name: path 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "1.8.2" 98 | sky_engine: 99 | dependency: transitive 100 | description: flutter 101 | source: sdk 102 | version: "0.0.99" 103 | source_span: 104 | dependency: transitive 105 | description: 106 | name: source_span 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "1.9.0" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.10.0" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.1" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.2.1" 138 | test_api: 139 | dependency: transitive 140 | description: 141 | name: test_api 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "0.4.12" 145 | vector_math: 146 | dependency: transitive 147 | description: 148 | name: vector_math 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "2.1.2" 152 | sdks: 153 | dart: ">=2.17.0-0 <3.0.0" 154 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | example 33 | 34 | 35 | 36 | 39 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /lib/chip_display/multi_select_chip_display.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../util/horizontal_scrollbar.dart'; 3 | import '../util/multi_select_item.dart'; 4 | 5 | /// A widget meant to display selected values as chips. 6 | // ignore: must_be_immutable 7 | class MultiSelectChipDisplay extends StatelessWidget { 8 | /// The source list of selected items. 9 | final List?>? items; 10 | 11 | /// Fires when a chip is tapped. 12 | final Function(V)? onTap; 13 | 14 | /// Set the chip color. 15 | final Color? chipColor; 16 | 17 | /// Change the alignment of the chips. 18 | final Alignment? alignment; 19 | 20 | /// Style the Container that makes up the chip display. 21 | final BoxDecoration? decoration; 22 | 23 | /// Style the text on the chips. 24 | final TextStyle? textStyle; 25 | 26 | /// A function that sets the color of selected items based on their value. 27 | final Color? Function(V)? colorator; 28 | 29 | /// An icon to display prior to the chip's label. 30 | final Icon? icon; 31 | 32 | /// Set a ShapeBorder. Typically a RoundedRectangularBorder. 33 | final ShapeBorder? shape; 34 | 35 | /// Enables horizontal scrolling. 36 | final bool scroll; 37 | 38 | /// Enables the scrollbar when scroll is `true`. 39 | final HorizontalScrollBar? scrollBar; 40 | 41 | final ScrollController _scrollController = ScrollController(); 42 | 43 | /// Set a fixed height. 44 | final double? height; 45 | 46 | /// Set the width of the chips. 47 | final double? chipWidth; 48 | 49 | bool? disabled; 50 | 51 | MultiSelectChipDisplay({ 52 | this.items, 53 | this.onTap, 54 | this.chipColor, 55 | this.alignment, 56 | this.decoration, 57 | this.textStyle, 58 | this.colorator, 59 | this.icon, 60 | this.shape, 61 | this.scroll = false, 62 | this.scrollBar, 63 | this.height, 64 | this.chipWidth, 65 | }) { 66 | this.disabled = false; 67 | } 68 | 69 | MultiSelectChipDisplay.none({ 70 | this.items = const [], 71 | this.disabled = true, 72 | this.onTap, 73 | this.chipColor, 74 | this.alignment, 75 | this.decoration, 76 | this.textStyle, 77 | this.colorator, 78 | this.icon, 79 | this.shape, 80 | this.scroll = false, 81 | this.scrollBar, 82 | this.height, 83 | this.chipWidth, 84 | }); 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | if (items == null || items!.isEmpty) return Container(); 89 | return Container( 90 | decoration: decoration, 91 | alignment: alignment ?? Alignment.centerLeft, 92 | padding: EdgeInsets.symmetric(horizontal: scroll ? 0 : 10), 93 | child: scroll 94 | ? Container( 95 | width: MediaQuery.of(context).size.width, 96 | height: height ?? MediaQuery.of(context).size.height * 0.08, 97 | child: scrollBar != null 98 | ? Scrollbar( 99 | thumbVisibility: scrollBar!.isAlwaysShown, 100 | controller: _scrollController, 101 | child: ListView.builder( 102 | controller: _scrollController, 103 | scrollDirection: Axis.horizontal, 104 | itemCount: items!.length, 105 | itemBuilder: (ctx, index) { 106 | return _buildItem(items![index]!, context); 107 | }, 108 | ), 109 | ) 110 | : ListView.builder( 111 | controller: _scrollController, 112 | scrollDirection: Axis.horizontal, 113 | itemCount: items!.length, 114 | itemBuilder: (ctx, index) { 115 | return _buildItem(items![index]!, context); 116 | }, 117 | ), 118 | ) 119 | : Wrap( 120 | children: items != null 121 | ? items!.map((item) => _buildItem(item!, context)).toList() 122 | : [ 123 | Container(), 124 | ], 125 | ), 126 | ); 127 | } 128 | 129 | Widget _buildItem(MultiSelectItem item, BuildContext context) { 130 | return Container( 131 | padding: const EdgeInsets.all(2.0), 132 | child: ChoiceChip( 133 | shape: shape as OutlinedBorder?, 134 | avatar: icon != null 135 | ? Icon( 136 | icon!.icon, 137 | color: colorator != null && colorator!(item.value) != null 138 | ? colorator!(item.value)!.withOpacity(1) 139 | : icon!.color ?? Theme.of(context).primaryColor, 140 | ) 141 | : null, 142 | label: Container( 143 | width: chipWidth, 144 | child: Text( 145 | item.label, 146 | overflow: TextOverflow.ellipsis, 147 | style: TextStyle( 148 | color: colorator != null && colorator!(item.value) != null 149 | ? textStyle != null 150 | ? textStyle!.color ?? colorator!(item.value) 151 | : colorator!(item.value) 152 | : textStyle != null && textStyle!.color != null 153 | ? textStyle!.color 154 | : chipColor != null 155 | ? chipColor!.withOpacity(1) 156 | : null, 157 | fontSize: textStyle != null ? textStyle!.fontSize : null, 158 | ), 159 | ), 160 | ), 161 | selected: items!.contains(item), 162 | selectedColor: colorator != null && colorator!(item.value) != null 163 | ? colorator!(item.value) 164 | : chipColor != null 165 | ? chipColor 166 | : Theme.of(context).primaryColor.withOpacity(0.33), 167 | onSelected: (_) { 168 | if (onTap != null) onTap!(item.value); 169 | }, 170 | ), 171 | ); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Multi Select Flutter 3 | 4 | [![Pub Version](https://img.shields.io/pub/v/multi_select_flutter.svg)](https://pub.dev/packages/multi_select_flutter) 5 | 6 | Multi Select Flutter is a package for creating multi-select widgets in a variety of ways. 7 | 8 | |
Dialog |
BottomSheet |
ChoiceChip | 9 | | :---: | :---: | :---: | 10 | 11 | ## Features 12 | - Supports FormField features like validator. 13 | - Neutral default design. 14 | - Dialog, BottomSheet, or ChoiceChip style widgets. 15 | - Make your multi select `searchable` for larger lists. 16 | 17 | ## Usage 18 | 19 | ### MultiSelectDialogField / MultiSelectBottomSheetField 20 | 21 | 22 | These widgets provide an InkWell button which open the dialog or bottom sheet and are equipped with FormField features. You can customize it using the provided parameters. 23 | 24 | To store the selected values, you can use the `onConfirm` parameter. You could also use `onSelectionChanged` for this. 25 | 26 | By default these widgets render a MultiSelectChipDisplay below the field. This can be overridden with the `chipDisplay` parameter or removed completely by using `chipDisplay: MultiSelectChipDisplay.none()`. 27 | 28 | ```dart 29 | MultiSelectDialogField( 30 | items: _animals.map((e) => MultiSelectItem(e, e.name)).toList(), 31 | listType: MultiSelectListType.CHIP, 32 | onConfirm: (values) { 33 | _selectedAnimals = values; 34 | }, 35 | ), 36 | ``` 37 | 38 | ### MultiSelectDialog / MultiSelectBottomSheet 39 |             40 | 41 | If you prefer to create your own button for opening the dialog or bottom sheet, you may do so and then make a call to a function like this: 42 | 43 | `MultiSelectDialog` can be used in the builder of `showDialog()`. 44 | ```dart 45 | void _showMultiSelect(BuildContext context) async { 46 | await showDialog( 47 | context: context, 48 | builder: (ctx) { 49 | return MultiSelectDialog( 50 | items: _items, 51 | initialValue: _selectedAnimals, 52 | onConfirm: (values) {...}, 53 | ); 54 | }, 55 | ); 56 | } 57 | ``` 58 | 59 | `MultiSelectBottomSheet` can be used in the builder of `showModalBottomSheet()`. 60 | ```dart 61 | void _showMultiSelect(BuildContext context) async { 62 | await showModalBottomSheet( 63 | isScrollControlled: true, // required for min/max child size 64 | context: context, 65 | builder: (ctx) { 66 | return MultiSelectBottomSheet( 67 | items: _items, 68 | initialValue: _selectedAnimals, 69 | onConfirm: (values) {...}, 70 | maxChildSize: 0.8, 71 | ); 72 | }, 73 | ); 74 | } 75 | ``` 76 | 77 | ### MultiSelectChipDisplay 78 | 79 | 80 | To display the selected items, this widget can be used alongside your own button or it can be specified as a `chipDisplay` parameter of widgets like `MultiSelectDialogField`. 81 | 82 | You can also remove items from the source list in the onTap function. 83 | 84 | ```dart 85 | MultiSelectChipDisplay( 86 | items: _selectedAnimals.map((e) => MultiSelectItem(e, e)).toList(), 87 | onTap: (value) { 88 | setState(() { 89 | _selectedAnimals.remove(value); 90 | }); 91 | }, 92 | ), 93 | ``` 94 | A MultiSelectChipDisplay that is part of a MultiSelectDialogField still renders outside the BoxDecoration of the MultiSelectDialogField as seen here: 95 | 96 | chipDisplay 97 | 98 | If you want to encapsulate the MultiSelectChipDisplay, wrap the MultiSelectDialogField in a Container and apply the decoration to that instead: 99 | 100 | ```dart 101 | Container( 102 | decoration: BoxDecoration(...), 103 | child: MultiSelectDialogField( 104 | items: _items, 105 | chipDisplay: MultiSelectChipDisplay(...), 106 | ), 107 | ), 108 | ``` 109 | chipDisplay 110 | 111 | ### MultiSelectChipField 112 | chipField 113 | 114 | This widget is similar to MultiSelectChipDisplay, except these chips are the primary interface for selecting items. 115 | ```dart 116 | MultiSelectChipField( 117 | items: _items, 118 | icon: Icon(Icons.check), 119 | onTap: (values) { 120 | _selectedAnimals = values; 121 | }, 122 | ), 123 | ``` 124 | #### Using `itemBuilder` to create custom items: 125 | ```dart 126 | MultiSelectChipField( 127 | items: _items, 128 | key: _multiSelectKey, 129 | validator: (values) {...} 130 | itemBuilder: (item, state) { 131 | // return your custom widget here 132 | return InkWell( 133 | onTap: () { 134 | _selectedAnimals.contains(item.value) 135 | ? _selectedAnimals.remove(item.value) 136 | : _selectedAnimals.add(item.value); 137 | state.didChange(_selectedAnimals); 138 | _multiSelectKey.currentState.validate(); 139 | }, 140 | child: Text(item.value.name), 141 | ); 142 | }, 143 | ), 144 | ``` 145 | The `itemBuilder` param takes a function that will create a widget for each of the provided `items`. 146 | 147 | In order to use validator and other FormField features with custom widgets, you must call `state.didChange(_updatedList)` any time the list of selected items is updated. 148 | 149 | #### Using `scrollControl` to auto scroll: 150 | ```dart 151 | MultiSelectChipField( 152 | items: _items, 153 | scrollControl: (controller) { 154 | _startAnimation(controller); 155 | }, 156 | ) 157 | 158 | // waits 5 seconds, scrolls to end slow, then back fast 159 | void _startAnimation(ScrollController controller) { 160 | // when using more than one animation, use async/await 161 | Future.delayed(const Duration(milliseconds: 5000), () async { 162 | await controller.animateTo( 163 | controller.position.maxScrollExtent, 164 | duration: Duration(milliseconds: 8000), 165 | curve: Curves.linear); 166 | 167 | await controller.animateTo( 168 | controller.position.minScrollExtent, 169 | duration: Duration(milliseconds: 1250), 170 | curve: Curves.fastLinearToSlowEaseIn); 171 | }); 172 | } 173 | ``` 174 | 175 | ## Contributing 176 | 177 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:multi_select_flutter/multi_select_flutter.dart'; 3 | 4 | void main() { 5 | runApp(MyApp()); 6 | } 7 | 8 | class MyApp extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return MaterialApp( 12 | title: 'Flutter Multi Select', 13 | theme: ThemeData( 14 | primarySwatch: Colors.purple, 15 | visualDensity: VisualDensity.adaptivePlatformDensity, 16 | ), 17 | home: MyHomePage(title: 'Flutter Multi Select'), 18 | ); 19 | } 20 | } 21 | 22 | class Animal { 23 | final int id; 24 | final String name; 25 | 26 | Animal({ 27 | this.id, 28 | this.name, 29 | }); 30 | } 31 | 32 | class MyHomePage extends StatefulWidget { 33 | MyHomePage({Key key, this.title}) : super(key: key); 34 | final String title; 35 | @override 36 | _MyHomePageState createState() => _MyHomePageState(); 37 | } 38 | 39 | class _MyHomePageState extends State { 40 | static List _animals = [ 41 | Animal(id: 1, name: "Lion"), 42 | Animal(id: 2, name: "Flamingo"), 43 | Animal(id: 3, name: "Hippo"), 44 | Animal(id: 4, name: "Horse"), 45 | Animal(id: 5, name: "Tiger"), 46 | Animal(id: 6, name: "Penguin"), 47 | Animal(id: 7, name: "Spider"), 48 | Animal(id: 8, name: "Snake"), 49 | Animal(id: 9, name: "Bear"), 50 | Animal(id: 10, name: "Beaver"), 51 | Animal(id: 11, name: "Cat"), 52 | Animal(id: 12, name: "Fish"), 53 | Animal(id: 13, name: "Rabbit"), 54 | Animal(id: 14, name: "Mouse"), 55 | Animal(id: 15, name: "Dog"), 56 | Animal(id: 16, name: "Zebra"), 57 | Animal(id: 17, name: "Cow"), 58 | Animal(id: 18, name: "Frog"), 59 | Animal(id: 19, name: "Blue Jay"), 60 | Animal(id: 20, name: "Moose"), 61 | Animal(id: 21, name: "Gecko"), 62 | Animal(id: 22, name: "Kangaroo"), 63 | Animal(id: 23, name: "Shark"), 64 | Animal(id: 24, name: "Crocodile"), 65 | Animal(id: 25, name: "Owl"), 66 | Animal(id: 26, name: "Dragonfly"), 67 | Animal(id: 27, name: "Dolphin"), 68 | ]; 69 | final _items = _animals 70 | .map((animal) => MultiSelectItem(animal, animal.name)) 71 | .toList(); 72 | //List _selectedAnimals = []; 73 | List _selectedAnimals2 = []; 74 | List _selectedAnimals3 = []; 75 | //List _selectedAnimals4 = []; 76 | List _selectedAnimals5 = []; 77 | final _multiSelectKey = GlobalKey(); 78 | 79 | @override 80 | void initState() { 81 | _selectedAnimals5 = _animals; 82 | super.initState(); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return Scaffold( 88 | appBar: AppBar( 89 | title: Text(widget.title), 90 | ), 91 | body: SingleChildScrollView( 92 | child: Container( 93 | alignment: Alignment.center, 94 | padding: EdgeInsets.all(20), 95 | child: Column( 96 | children: [ 97 | SizedBox(height: 40), 98 | //################################################################################################ 99 | // Rounded blue MultiSelectDialogField 100 | //################################################################################################ 101 | MultiSelectDialogField( 102 | items: _items, 103 | title: Text("Animals"), 104 | selectedColor: Colors.blue, 105 | decoration: BoxDecoration( 106 | color: Colors.blue.withOpacity(0.1), 107 | borderRadius: BorderRadius.all(Radius.circular(40)), 108 | border: Border.all( 109 | color: Colors.blue, 110 | width: 2, 111 | ), 112 | ), 113 | buttonIcon: Icon( 114 | Icons.pets, 115 | color: Colors.blue, 116 | ), 117 | buttonText: Text( 118 | "Favorite Animals", 119 | style: TextStyle( 120 | color: Colors.blue[800], 121 | fontSize: 16, 122 | ), 123 | ), 124 | onConfirm: (results) { 125 | //_selectedAnimals = results; 126 | }, 127 | ), 128 | SizedBox(height: 50), 129 | //################################################################################################ 130 | // This MultiSelectBottomSheetField has no decoration, but is instead wrapped in a Container that has 131 | // decoration applied. This allows the ChipDisplay to render inside the same Container. 132 | //################################################################################################ 133 | Container( 134 | decoration: BoxDecoration( 135 | color: Theme.of(context).primaryColor.withOpacity(.4), 136 | border: Border.all( 137 | color: Theme.of(context).primaryColor, 138 | width: 2, 139 | ), 140 | ), 141 | child: Column( 142 | children: [ 143 | MultiSelectBottomSheetField( 144 | initialChildSize: 0.4, 145 | listType: MultiSelectListType.CHIP, 146 | searchable: true, 147 | buttonText: Text("Favorite Animals"), 148 | title: Text("Animals"), 149 | items: _items, 150 | onConfirm: (values) { 151 | _selectedAnimals2 = values; 152 | }, 153 | chipDisplay: MultiSelectChipDisplay( 154 | onTap: (value) { 155 | setState(() { 156 | _selectedAnimals2.remove(value); 157 | }); 158 | }, 159 | ), 160 | ), 161 | _selectedAnimals2 == null || _selectedAnimals2.isEmpty 162 | ? Container( 163 | padding: EdgeInsets.all(10), 164 | alignment: Alignment.centerLeft, 165 | child: Text( 166 | "None selected", 167 | style: TextStyle(color: Colors.black54), 168 | )) 169 | : Container(), 170 | ], 171 | ), 172 | ), 173 | SizedBox(height: 40), 174 | //################################################################################################ 175 | // MultiSelectBottomSheetField with validators 176 | //################################################################################################ 177 | MultiSelectBottomSheetField( 178 | key: _multiSelectKey, 179 | initialChildSize: 0.7, 180 | maxChildSize: 0.95, 181 | title: Text("Animals"), 182 | buttonText: Text("Favorite Animals"), 183 | items: _items, 184 | searchable: true, 185 | validator: (values) { 186 | if (values == null || values.isEmpty) { 187 | return "Required"; 188 | } 189 | List names = values.map((e) => e.name).toList(); 190 | if (names.contains("Frog")) { 191 | return "Frogs are weird!"; 192 | } 193 | return null; 194 | }, 195 | onConfirm: (values) { 196 | setState(() { 197 | _selectedAnimals3 = values; 198 | }); 199 | _multiSelectKey.currentState.validate(); 200 | }, 201 | chipDisplay: MultiSelectChipDisplay( 202 | onTap: (item) { 203 | setState(() { 204 | _selectedAnimals3.remove(item); 205 | }); 206 | _multiSelectKey.currentState.validate(); 207 | }, 208 | ), 209 | ), 210 | SizedBox(height: 40), 211 | //################################################################################################ 212 | // MultiSelectChipField 213 | //################################################################################################ 214 | MultiSelectChipField( 215 | items: _items, 216 | initialValue: [_animals[4], _animals[7], _animals[9]], 217 | title: Text("Animals"), 218 | headerColor: Colors.blue.withOpacity(0.5), 219 | decoration: BoxDecoration( 220 | border: Border.all(color: Colors.blue[700], width: 1.8), 221 | ), 222 | selectedChipColor: Colors.blue.withOpacity(0.5), 223 | selectedTextStyle: TextStyle(color: Colors.blue[800]), 224 | onTap: (values) { 225 | //_selectedAnimals4 = values; 226 | }, 227 | ), 228 | SizedBox(height: 40), 229 | //################################################################################################ 230 | // MultiSelectDialogField with initial values 231 | //################################################################################################ 232 | MultiSelectDialogField( 233 | onConfirm: (val) { 234 | _selectedAnimals5 = val; 235 | }, 236 | dialogWidth: MediaQuery.of(context).size.width * 0.7, 237 | items: _items, 238 | initialValue: 239 | _selectedAnimals5, // setting the value of this in initState() to pre-select values. 240 | ), 241 | ], 242 | ), 243 | ), 244 | ), 245 | ); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /lib/dialog/mult_select_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../util/multi_select_actions.dart'; 3 | import '../util/multi_select_item.dart'; 4 | import '../util/multi_select_list_type.dart'; 5 | 6 | /// A dialog containing either a classic checkbox style list, or a chip style list. 7 | class MultiSelectDialog extends StatefulWidget with MultiSelectActions { 8 | /// List of items to select from. 9 | final List> items; 10 | 11 | /// The list of selected values before interaction. 12 | final List initialValue; 13 | 14 | /// The text at the top of the dialog. 15 | final Widget? title; 16 | 17 | /// Fires when the an item is selected / unselected. 18 | final void Function(List)? onSelectionChanged; 19 | 20 | /// Fires when confirm is tapped. 21 | final void Function(List)? onConfirm; 22 | 23 | /// Toggles search functionality. Default is false. 24 | final bool searchable; 25 | 26 | /// Text on the confirm button. 27 | final Text? confirmText; 28 | 29 | /// Text on the cancel button. 30 | final Text? cancelText; 31 | 32 | /// An enum that determines which type of list to render. 33 | final MultiSelectListType? listType; 34 | 35 | /// Sets the color of the checkbox or chip when it's selected. 36 | final Color? selectedColor; 37 | 38 | /// Sets a fixed height on the dialog. 39 | final double? height; 40 | 41 | /// Sets a fixed width on the dialog. 42 | final double? width; 43 | 44 | /// Set the placeholder text of the search field. 45 | final String? searchHint; 46 | 47 | /// A function that sets the color of selected items based on their value. 48 | /// It will either set the chip color, or the checkbox color depending on the list type. 49 | final Color? Function(T)? colorator; 50 | 51 | /// The background color of the dialog. 52 | final Color? backgroundColor; 53 | 54 | /// The color of the chip body or checkbox border while not selected. 55 | final Color? unselectedColor; 56 | 57 | /// Icon button that shows the search field. 58 | final Icon? searchIcon; 59 | 60 | /// Icon button that hides the search field 61 | final Icon? closeSearchIcon; 62 | 63 | /// Style the text on the chips or list tiles. 64 | final TextStyle? itemsTextStyle; 65 | 66 | /// Style the text on the selected chips or list tiles. 67 | final TextStyle? selectedItemsTextStyle; 68 | 69 | /// Style the search text. 70 | final TextStyle? searchTextStyle; 71 | 72 | /// Style the search hint. 73 | final TextStyle? searchHintStyle; 74 | 75 | /// Moves the selected items to the top of the list. 76 | final bool separateSelectedItems; 77 | 78 | /// Set the color of the check in the checkbox 79 | final Color? checkColor; 80 | 81 | MultiSelectDialog({ 82 | required this.items, 83 | required this.initialValue, 84 | this.title, 85 | this.onSelectionChanged, 86 | this.onConfirm, 87 | this.listType, 88 | this.searchable = false, 89 | this.confirmText, 90 | this.cancelText, 91 | this.selectedColor, 92 | this.searchHint, 93 | this.height, 94 | this.width, 95 | this.colorator, 96 | this.backgroundColor, 97 | this.unselectedColor, 98 | this.searchIcon, 99 | this.closeSearchIcon, 100 | this.itemsTextStyle, 101 | this.searchHintStyle, 102 | this.searchTextStyle, 103 | this.selectedItemsTextStyle, 104 | this.separateSelectedItems = false, 105 | this.checkColor, 106 | }); 107 | 108 | @override 109 | State createState() => _MultiSelectDialogState(items); 110 | } 111 | 112 | class _MultiSelectDialogState extends State> { 113 | List _selectedValues = []; 114 | bool _showSearch = false; 115 | List> _items; 116 | 117 | _MultiSelectDialogState(this._items); 118 | 119 | @override 120 | void initState() { 121 | super.initState(); 122 | _selectedValues.addAll(widget.initialValue); 123 | 124 | for (int i = 0; i < _items.length; i++) { 125 | _items[i].selected = false; 126 | if (_selectedValues.contains(_items[i].value)) { 127 | _items[i].selected = true; 128 | } 129 | } 130 | 131 | if (widget.separateSelectedItems) { 132 | _items = widget.separateSelected(_items); 133 | } 134 | } 135 | 136 | /// Returns a CheckboxListTile 137 | Widget _buildListItem(MultiSelectItem item) { 138 | return Theme( 139 | data: ThemeData( 140 | unselectedWidgetColor: widget.unselectedColor ?? Colors.black54, 141 | ), 142 | child: CheckboxListTile( 143 | checkColor: widget.checkColor, 144 | value: item.selected, 145 | activeColor: widget.colorator != null 146 | ? widget.colorator!(item.value) ?? widget.selectedColor 147 | : widget.selectedColor, 148 | title: Text( 149 | item.label, 150 | style: item.selected 151 | ? widget.selectedItemsTextStyle 152 | : widget.itemsTextStyle, 153 | ), 154 | controlAffinity: ListTileControlAffinity.leading, 155 | onChanged: (checked) { 156 | setState(() { 157 | _selectedValues = widget.onItemCheckedChange( 158 | _selectedValues, item.value, checked!); 159 | 160 | if (checked) { 161 | item.selected = true; 162 | } else { 163 | item.selected = false; 164 | } 165 | if (widget.separateSelectedItems) { 166 | _items = widget.separateSelected(_items); 167 | } 168 | }); 169 | if (widget.onSelectionChanged != null) { 170 | widget.onSelectionChanged!(_selectedValues); 171 | } 172 | }, 173 | ), 174 | ); 175 | } 176 | 177 | /// Returns a ChoiceChip 178 | Widget _buildChipItem(MultiSelectItem item) { 179 | return Container( 180 | padding: const EdgeInsets.all(2.0), 181 | child: ChoiceChip( 182 | backgroundColor: widget.unselectedColor, 183 | selectedColor: widget.colorator?.call(item.value) ?? 184 | widget.selectedColor ?? 185 | Theme.of(context).primaryColor.withOpacity(0.35), 186 | label: Text( 187 | item.label, 188 | style: item.selected 189 | ? TextStyle( 190 | color: widget.selectedItemsTextStyle?.color ?? 191 | widget.colorator?.call(item.value) ?? 192 | widget.selectedColor?.withOpacity(1) ?? 193 | Theme.of(context).primaryColor, 194 | fontSize: widget.selectedItemsTextStyle?.fontSize, 195 | ) 196 | : widget.itemsTextStyle, 197 | ), 198 | selected: item.selected, 199 | onSelected: (checked) { 200 | if (checked) { 201 | item.selected = true; 202 | } else { 203 | item.selected = false; 204 | } 205 | setState(() { 206 | _selectedValues = widget.onItemCheckedChange( 207 | _selectedValues, item.value, checked); 208 | }); 209 | if (widget.onSelectionChanged != null) { 210 | widget.onSelectionChanged!(_selectedValues); 211 | } 212 | }, 213 | ), 214 | ); 215 | } 216 | 217 | @override 218 | Widget build(BuildContext context) { 219 | return AlertDialog( 220 | backgroundColor: widget.backgroundColor, 221 | title: widget.searchable == false 222 | ? widget.title ?? const Text("Select") 223 | : Row( 224 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 225 | children: [ 226 | _showSearch 227 | ? Expanded( 228 | child: Container( 229 | padding: EdgeInsets.only(left: 10), 230 | child: TextField( 231 | style: widget.searchTextStyle, 232 | decoration: InputDecoration( 233 | hintStyle: widget.searchHintStyle, 234 | hintText: widget.searchHint ?? "Search", 235 | focusedBorder: UnderlineInputBorder( 236 | borderSide: BorderSide( 237 | color: widget.selectedColor ?? 238 | Theme.of(context).primaryColor, 239 | ), 240 | ), 241 | ), 242 | onChanged: (val) { 243 | List> filteredList = []; 244 | filteredList = 245 | widget.updateSearchQuery(val, widget.items); 246 | setState(() { 247 | if (widget.separateSelectedItems) { 248 | _items = 249 | widget.separateSelected(filteredList); 250 | } else { 251 | _items = filteredList; 252 | } 253 | }); 254 | }, 255 | ), 256 | ), 257 | ) 258 | : widget.title ?? Text("Select"), 259 | IconButton( 260 | icon: _showSearch 261 | ? widget.closeSearchIcon ?? Icon(Icons.close) 262 | : widget.searchIcon ?? Icon(Icons.search), 263 | onPressed: () { 264 | setState(() { 265 | _showSearch = !_showSearch; 266 | if (!_showSearch) { 267 | if (widget.separateSelectedItems) { 268 | _items = widget.separateSelected(widget.items); 269 | } else { 270 | _items = widget.items; 271 | } 272 | } 273 | }); 274 | }, 275 | ), 276 | ], 277 | ), 278 | contentPadding: 279 | widget.listType == null || widget.listType == MultiSelectListType.LIST 280 | ? EdgeInsets.only(top: 12.0) 281 | : EdgeInsets.all(20), 282 | content: Container( 283 | height: widget.height, 284 | width: widget.width ?? MediaQuery.of(context).size.width * 0.73, 285 | child: widget.listType == null || 286 | widget.listType == MultiSelectListType.LIST 287 | ? ListView.builder( 288 | itemCount: _items.length, 289 | itemBuilder: (context, index) { 290 | return _buildListItem(_items[index]); 291 | }, 292 | ) 293 | : SingleChildScrollView( 294 | child: Wrap( 295 | children: _items.map(_buildChipItem).toList(), 296 | ), 297 | ), 298 | ), 299 | actions: [ 300 | TextButton( 301 | child: widget.cancelText ?? 302 | Text( 303 | "CANCEL", 304 | style: TextStyle( 305 | color: (widget.selectedColor != null && 306 | widget.selectedColor != Colors.transparent) 307 | ? widget.selectedColor!.withOpacity(1) 308 | : Theme.of(context).primaryColor, 309 | ), 310 | ), 311 | onPressed: () { 312 | widget.onCancelTap(context, widget.initialValue); 313 | }, 314 | ), 315 | TextButton( 316 | child: widget.confirmText ?? 317 | Text( 318 | 'OK', 319 | style: TextStyle( 320 | color: (widget.selectedColor != null && 321 | widget.selectedColor != Colors.transparent) 322 | ? widget.selectedColor!.withOpacity(1) 323 | : Theme.of(context).primaryColor, 324 | ), 325 | ), 326 | onPressed: () { 327 | widget.onConfirmTap(context, _selectedValues, widget.onConfirm); 328 | }, 329 | ) 330 | ], 331 | ); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | ## [4.1.3] - 2022-11-27 7 | ### Changed 8 | - make initialValue non-nullable 9 | - add didUpdateWidget that sets the selected items if initialValue changed 10 | - add isDismissable parameter 11 | - fixed 'items still selected after cancel' 12 | 13 | ## [4.1.2] - 2022-03-14 14 | ### Changed 15 | - Merged [PR #87](https://github.com/CHB61/multi_select_flutter/pull/87). Thanks @benyaminbeyzaie 16 | 17 | ## [4.1.1] - 2022-03-14 18 | ### Changed 19 | - Created a bug in 4.0.0 by removing the 'collection' dependency. It's only used to call firstWhereOrNull in _buildInheritedChipDisplay in both MultiSelectDialogField and MultiSelectBottomSheetField. This function is meant to automatically build the chips, but it may be changed or removed in the future as I don't think it is really necessary, and has caused some issues. For now, the package still depends on collection. 20 | 21 | ## [4.1.0] - 2022-03-14 22 | ### Changed 23 | - BREAKING: re-named the 'width' and 'height' fields to dialogWidth and dialogHeight. 24 | - Allow to adjust the width of the dialog. 25 | 26 | ### Added 27 | - param 'separateSelectedItems' which can be used with LIST type only. 28 | 29 | ## [4.0.0] - 2021-03-20 30 | ### Changed 31 | - Added null safety. Thanks to @ihancock for taking the initiative to apply this. 32 | 33 | ## [3.1.8] - 2021-02-15 34 | ### Fixed 35 | - [Slow with big list](https://github.com/CHB61/multi_select_flutter/issues/18). Using a ListView.builder solves this but haven't found a way to use ListView.builder when the listType is set to MultiSelectListType.CHIP. Currently when this is set, the widget renders a Wrap inside of a SingleChildScrollView. For now, if you have a big list, do not use `MultiSelectListType.CHIP`. 36 | ## [3.1.7] - 2021-02-14 37 | ### Changed 38 | - title param type from Text to Widget 39 | ### Fixed 40 | - Missing generic type `` on MultiSelectChipDisplay declarations 41 | - MultiSelectDialogField.dart line 33 in MultiSelectDialogField 42 | - MultiSelectDialogField.dart line 181 in _MultiSelectDialogFieldView 43 | - MultiSelectBottomSheetField.dart line 69 in MultiSelectBottomSheetField 44 | - MultiSelectBottomSheetField.dart line 209 _MultiSelectBottomSheetFieldView 45 | 46 | This would cause a type error if you were to specify the type of a MultiSelectChipDisplay that's part of a MultiSelectDialogField or MultiSelectBottomSheetField. 47 | 48 | ```dart 49 | MultiSelectBottomSheetField( 50 | onConfirm: (values) { 51 | _selectedAnimals = values; 52 | }, 53 | items: _items, 54 | chipDisplay: MultiSelectChipDisplay( // supplying the runtime type here will cause error 55 | onTap: (item) { 56 | _selectedAnimals.remove(item); 57 | return _selectedAnimals; 58 | }, 59 | ), 60 | ) 61 | ``` 62 | The type being supplied plus returning a List would mean that the onTap function is now `List Function(Animal)` 63 | However, since the declaration of the chipDisplay was missing the type (ex. MultiSelectChipDisplay chipDisplay;), it is expecting onTap to be 64 | `dynamic Function(dynamic)`. 65 | ## [3.1.6] - 2020-12-05 66 | ### Changed 67 | - Autofocus search textfield when search icon is tapped. 68 | 69 | ## [3.1.5] - 2020-12-01 70 | ### Fixed 71 | - Readme tables 72 | ## [3.1.4] - 2020-11-30 73 | ### Added 74 | - A constructor MultiSelectChipDisplay.none() which disables the default chipDisplay that is part of MultiSelectDialogField and 75 | MultiSelectBottomSheetField. 76 | ### Fixed 77 | - When using a MultiSelectDialogField with pre-selected values, can't remove from the chipDisplay until after first confirm. 78 | - The onConfirm function provides a reference to the list of selected values that MultiSelectDialogField uses. 79 | - If you have pre-selected values set, you can now simply return the list of updated values in the onTap of the 80 | MultiSelectChipDisplay. 81 | ## [3.1.3] - 2020-10-09 82 | ### Added 83 | - param `chipWidth` for MultiSelectChipDisplay and MultiSelectChipField. When this is set, overflowing text will show ellipses. 84 | 85 | 86 | ## [3.1.2] - 2020-10-09 87 | ### Added 88 | - param `scroll`, `scrollBar`, `height` for MultiSelectChipDisplay. 89 | 90 | ## [3.1.1] - 2020-10-08 91 | ### Added 92 | - param `showHeader` for MultiSelectChipField allowing it to be completely removed if so desired. 93 | 94 | ## [3.1.0] - 2020-10-04 95 | ### Fixed 96 | - `type Color is not a subtype of type bool` when selecting item in MultiSelectBottomSheet 97 | - These errors appear in cases that use conditional expressions like this: `widget.selectedColor ?? widget.var != null ? widget.var.func() : widget.otherVar`. 98 | - Show selected chips in MultiSelectChipDisplay when `initialValue` is set 99 | - Previously if you set the initialValue on a MultiSelectBottomSheetField, it would show the selected items in the bottomsheet 100 | but not in the chipDisplay. 101 | 102 | ### Added 103 | - param `checkColor` for dialog and bottomsheet widgets. 104 | 105 | ### Changed 106 | - BREAKING: Replace `chipColor` with `unselectedColor` for dialog and bottomsheet widgets. Apply the color to unselected checkbox border or the 107 | unselected chip body depending which `listType` is used. 108 | - BREAKING: Replace deprecated `autovalidate` with `autovalidateMode` on FormField widgets 109 | 110 | ## [3.0.1] - 2020-09-28 111 | ### Fixed 112 | - `type List is not a subtype of type bool` when using chipDisplay param on MultiSelectBottomSheet. 113 | 114 | ## [3.0.0] - 2020-09-27 115 | ### Added 116 | - MultiSelectChipField 117 | - Similar to MultiSelectChipDisplay except that it is the primary 118 | interface to select items 119 | - supports scroll or wrap, autoscroll, formfield features, searchable, custom items 120 | 121 | ### Changed 122 | - MultiSelectDialogField and MultiSelectBottomSheetField now come with a default `chipDisplay`. 123 | - User no longer needs `chipDisplay` param to have a MultiSelectChipDisplay, a MultiSelectDialogField with only `items` specified is enough. 124 | - User can override or remove it. To remove it, override with MultiSelectChipDisplay(items: []) 125 | - User no longer needs to specify `items` on MultiSelectChipDisplay when using it as a chipDisplay param 126 | - Combine Field/FormField widgets 127 | - Instead of having 2 widgets e.g. MultiSelectDialogField and MultiSelectDialogFormField, 128 | move the features of MultiSelectDialogFormField into MultiSelectDialogField. 129 | - Made MultiSelectChipDisplay stateless 130 | - Renamed searchPlaceholder to searchHint 131 | - Removed opacity param from MultiSelectChipDisplay. 132 | 133 | ## [2.3.0] - 2020-09-14 134 | ### Fixed 135 | - An error was being produced when using the `icon` param in MultiSelectChipDisplay with no `colorator` applied. 136 | - When no `title` is provided in MultiSelectDialogField, default title of Text("Select") should be provided, not a String of "Select" 137 | 138 | ### Changed 139 | - BREAKING: Removed `chipOpacity` from MultiSelectDialog and MultiSelectBottomSheet widgets. 140 | - can simply set the opacity along with the color. 141 | 142 | ### Added 143 | - Param `selectedItemsTextStyle` that applies to dialog / bottomsheet, list and chip. 144 | 145 | 146 | ## [2.2.0] - 2020-08-31 147 | ### Added 148 | A number of parameters that allow more customizations. 149 | - `backgroundColor` 150 | - `chipColor` 151 | - `chipOpacity` 152 | - `searchIcon`, `closeSearchIcon` 153 | - `itemsTextStyle`, `searchTextStyle`, `searchHintStyle` 154 | - `icon` and `shape` for MultiSelectChipDisplay 155 | 156 | ## [2.1.1] - 2020-08-27 157 | ### Changed 158 | - When colorator is applied to Field or FormField, apply the same colorator to the chipDisplay if there is one. 159 | Previously, when using a MultiSelectDialogField with a chipDisplay and you wanted the same effect both within 160 | the dialog and on the chipDisplay, you would have to define the colorator for both widgets. This is repetitive 161 | and not ideal. Now, the MultiSelectChipDisplay inherits the colorator from the parent field, and can still 162 | override that with the use of its own colorator. 163 | 164 | ## [2.1.0] - 2020-08-24 165 | ### Added 166 | - `colorator` param for all widgets. Set the color of individual items based on their value. 167 | - works like FormField's validator 168 | - takes a function in which you compare the value 169 | - return a color based on the value 170 | - return null if no conditions satisfied 171 | - applies to selected chips and checkboxes 172 | - `opacity` param for MultiSelectChipDisplay 173 | 174 | ### Changed 175 | - MultiSelectChipDisplay's `chipColor` param now automatically sets the opacity to 0.33 and sets the text to the same color but with full opacity. 176 | - previously you would have to set the chipColor to `Colors.blue.withOpacity(0.33)` 177 | and textStyle to `TextStyle(Colors.blue)` to achieve this simple look. 178 | - you can still override this if you want different colored text by using the `textStyle` param 179 | - and you can override the default 0.33 opacity of the chip body with the new `opacity` param 180 | - set the color of the FormField widgets bottom border to `selectedColor` if there is one 181 | - set the color of confirm/cancel buttons to `selectedColor` if there is one 182 | 183 | 184 | ## [2.0.3] - 2020-08-22 185 | ### Added 186 | - parameter `searchPlaceholder` to replace the default "Search" text. 187 | 188 | ### Fixed 189 | - `selectedColor` wasn't being passed to MultiSelectDialog when using MultiSelectDialogField or MultiSelectDialogFormField 190 | 191 | ## [2.0.2] - 2020-08-17 192 | ### Added 193 | - dartdoc comments 194 | 195 | ## [2.0.1] - 2020-08-17 196 | ### Added 197 | - `selectedColor` param which controls the color of the selected checkbox / chips within a dialog / bottomsheet 198 | - `height` param to MultiSelectDialog widgets. 199 | 200 | ### Changed 201 | - Set the default color of the confirmText and cancelText to primary for BottomSheet widgets. 202 | 203 | ### Fixed 204 | - onSelectionChanged wasn't being called for all widgets. 205 | 206 | ## [2.0.0] - 2020-08-16 207 | ### Added 208 | - `MultiSelectBottomSheet`, `MultiSelectBottomSheetField`, `MultiSelectBottomSheetFormField` 209 | - `barrierColor` param to MultiSelectDialog 210 | 211 | ### Changed 212 | - The addition of the MultiSelectBottomSheet widgets prompted a bit of a re-write in order to de-couple widgets. Didn't want the generic MultiSelectField to be responsible for both types (dialog, bottomsheet). The new structure makes more sense and is easier to work with. 213 | - MultiSelectField replaced with MultiSelectDialogField / MultiSelectBottomSheetField. 214 | - MultiSelectListDialog and MultiSelectChipDialog have been replaced with MultiSelectDialog. 215 | - dialogType replaced with listType, now accepts MultiSelectListType instead of MultiSelectDialogType. 216 | - Improved the look of validator text, matches the look of TextFormField validator style much more closely. 217 | - buttonText, confirmText, cancelText all now accept a Text widget instead of a string, allowing the removal of the textStyle param. 218 | 219 | ## [1.0.6] - 2020-08-14 220 | 221 | ### Fixed 222 | 223 | - Implement scrolling in MultiSelectChipDialog. 224 | - When the source list was large enough, the chip dialog would just overflow but it is now scrollable. 225 | 226 | ### Changed 227 | 228 | - `title` is no longer required for List / Chip dialogs, default to "Select". 229 | - `initialSelectedItems` is now required for List / Chip dialogs. 230 | - `items` is now required for MultiSelectChipDisplay. 231 | - Removed iconSize - can set icon size as param of Icon. 232 | - Removed `state` as a parameter of MultiSelectField and created a constructor `MultiSelectField.withState()` that gets called by MultiSelectFormField. 233 | - `state` never needs to be set by the user, and would do nothing if set explicitly, so it shouldn't be a named param on the default constructor. 234 | - Updated docs 235 | 236 | ## [1.0.5] - 2020-07-10 237 | 238 | ### Fixed 239 | 240 | - A bug was introduced in version 1.0.4 that caused the MultiSelectFormField to not highlight any of the selected values in the dialog. 241 | 242 | ### Added 243 | 244 | - Boolean parameter 'searchable'. Useful for larger lists, the searchable parameter enables a search icon in the dialog which shows a search bar that lets you query the list. 245 | - String parameters for 'confirmText' and 'cancelText'. This is important for users who want text other than 'OK' and 'CANCEL', especially for other languages and alphabets. 246 | 247 | ## [1.0.4] - 2020-07-06 248 | 249 | ### Changed 250 | 251 | - Allow user to set initial selected values for MultiSelectField and MultiSelectFormField by adding param 'initialValue' to MultiSelectField, and enabled the same functionality for MultiSelectFormField by passing its existing 'initialValue' param to the MultiSelectField's new 'initialValue' param. 252 | - MultiSelectFormField creates a MultiSelectField which never had an 'initialValue' param that could be set by the user. Even if 'initialValue' was set on the MultiSelectFormField, it never got passed to the MultiSelectField that it creates. Now it does. 253 | - Previously when using a MultiSelectField or FormField, the values in the MultiSelectListDialog / MultiSelectChipDialog were only being stored internally when a user confirms the values. Now the initial values can be set before any user interaction has occurred. 254 | - Another use case is when a MultiSelectField is being re-inserted into the widget tree (such as one inside of a PersistentBottomSheet), and if the developer wants the previously selected values to remain after the bottomsheet re-opens, they can use this parameter to achieve that. 255 | - Updated example app 256 | 257 | ## [1.0.3] - 2020-06-21 258 | 259 | ### Changed 260 | 261 | - Updated pubspec.yaml project description and added homepage link 262 | 263 | ## [1.0.2] - 2020-06-20 264 | 265 | ### Added 266 | 267 | - Example project 268 | 269 | - analysis_options.yaml 270 | 271 | ## [1.0.1] - 2020-06-20 272 | 273 | ### Changed 274 | 275 | - Added readme 276 | 277 | ## [1.0.0] - 2020-06-20 278 | 279 | ### Added 280 | 281 | - Creation of MultiSelect package 282 | 283 | - Widgets include MultiSelectListDialog, MultiSelectChipDialog, MultiSelectChipDisplay, MultiSelectField, MultiSelectFormField. -------------------------------------------------------------------------------- /lib/bottom_sheet/multi_select_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../util/multi_select_item.dart'; 3 | import '../util/multi_select_actions.dart'; 4 | import '../util/multi_select_list_type.dart'; 5 | 6 | /// A bottom sheet widget containing either a classic checkbox style list, or a chip style list. 7 | class MultiSelectBottomSheet extends StatefulWidget 8 | with MultiSelectActions { 9 | /// List of items to select from. 10 | final List> items; 11 | 12 | /// The list of selected values before interaction. 13 | final List initialValue; 14 | 15 | /// The text at the top of the BottomSheet. 16 | final Widget? title; 17 | 18 | /// Fires when the an item is selected / unselected. 19 | final void Function(List)? onSelectionChanged; 20 | 21 | /// Fires when confirm is tapped. 22 | final void Function(List)? onConfirm; 23 | 24 | /// Toggles search functionality. 25 | final bool searchable; 26 | 27 | /// Text on the confirm button. 28 | final Text? confirmText; 29 | 30 | /// Text on the cancel button. 31 | final Text? cancelText; 32 | 33 | /// An enum that determines which type of list to render. 34 | final MultiSelectListType? listType; 35 | 36 | /// Sets the color of the checkbox or chip when it's selected. 37 | final Color? selectedColor; 38 | 39 | /// Set the initial height of the BottomSheet. 40 | final double? initialChildSize; 41 | 42 | /// Set the minimum height threshold of the BottomSheet before it closes. 43 | final double? minChildSize; 44 | 45 | /// Set the maximum height of the BottomSheet. 46 | final double? maxChildSize; 47 | 48 | /// Set the placeholder text of the search field. 49 | final String? searchHint; 50 | 51 | /// A function that sets the color of selected items based on their value. 52 | /// It will either set the chip color, or the checkbox color depending on the list type. 53 | final Color? Function(T)? colorator; 54 | 55 | /// Color of the chip body or checkbox border while not selected. 56 | final Color? unselectedColor; 57 | 58 | /// Icon button that shows the search field. 59 | final Icon? searchIcon; 60 | 61 | /// Icon button that hides the search field 62 | final Icon? closeSearchIcon; 63 | 64 | /// Style the text on the chips or list tiles. 65 | final TextStyle? itemsTextStyle; 66 | 67 | /// Style the text on the selected chips or list tiles. 68 | final TextStyle? selectedItemsTextStyle; 69 | 70 | /// Style the search text. 71 | final TextStyle? searchTextStyle; 72 | 73 | /// Style the search hint. 74 | final TextStyle? searchHintStyle; 75 | 76 | /// Moves the selected items to the top of the list. 77 | final bool separateSelectedItems; 78 | 79 | /// Set the color of the check in the checkbox 80 | final Color? checkColor; 81 | 82 | MultiSelectBottomSheet({ 83 | required this.items, 84 | required this.initialValue, 85 | this.title, 86 | this.onSelectionChanged, 87 | this.onConfirm, 88 | this.listType, 89 | this.cancelText, 90 | this.confirmText, 91 | this.searchable = false, 92 | this.selectedColor, 93 | this.initialChildSize, 94 | this.minChildSize, 95 | this.maxChildSize, 96 | this.colorator, 97 | this.unselectedColor, 98 | this.searchIcon, 99 | this.closeSearchIcon, 100 | this.itemsTextStyle, 101 | this.searchTextStyle, 102 | this.searchHint, 103 | this.searchHintStyle, 104 | this.selectedItemsTextStyle, 105 | this.separateSelectedItems = false, 106 | this.checkColor, 107 | }); 108 | 109 | @override 110 | _MultiSelectBottomSheetState createState() => 111 | _MultiSelectBottomSheetState(items); 112 | } 113 | 114 | class _MultiSelectBottomSheetState extends State> { 115 | List _selectedValues = []; 116 | bool _showSearch = false; 117 | List> _items; 118 | 119 | _MultiSelectBottomSheetState(this._items); 120 | 121 | @override 122 | void initState() { 123 | super.initState(); 124 | _selectedValues.addAll(widget.initialValue); 125 | 126 | for (int i = 0; i < _items.length; i++) { 127 | _items[i].selected = false; 128 | if (_selectedValues.contains(_items[i].value)) { 129 | _items[i].selected = true; 130 | } 131 | } 132 | 133 | if (widget.separateSelectedItems) { 134 | _items = widget.separateSelected(_items); 135 | } 136 | } 137 | 138 | /// Returns a CheckboxListTile 139 | Widget _buildListItem(MultiSelectItem item) { 140 | return Theme( 141 | data: ThemeData( 142 | unselectedWidgetColor: widget.unselectedColor ?? Colors.black54, 143 | ), 144 | child: CheckboxListTile( 145 | checkColor: widget.checkColor, 146 | value: item.selected, 147 | activeColor: widget.colorator != null 148 | ? widget.colorator!(item.value) ?? widget.selectedColor 149 | : widget.selectedColor, 150 | title: Text( 151 | item.label, 152 | style: item.selected 153 | ? widget.selectedItemsTextStyle 154 | : widget.itemsTextStyle, 155 | ), 156 | controlAffinity: ListTileControlAffinity.leading, 157 | onChanged: (checked) { 158 | setState(() { 159 | _selectedValues = widget.onItemCheckedChange( 160 | _selectedValues, item.value, checked!); 161 | 162 | if (checked) { 163 | item.selected = true; 164 | } else { 165 | item.selected = false; 166 | } 167 | if (widget.separateSelectedItems) { 168 | _items = widget.separateSelected(_items); 169 | } 170 | }); 171 | if (widget.onSelectionChanged != null) { 172 | widget.onSelectionChanged!(_selectedValues); 173 | } 174 | }, 175 | ), 176 | ); 177 | } 178 | 179 | /// Returns a ChoiceChip 180 | Widget _buildChipItem(MultiSelectItem item) { 181 | return Container( 182 | padding: const EdgeInsets.all(2.0), 183 | child: ChoiceChip( 184 | backgroundColor: widget.unselectedColor, 185 | selectedColor: 186 | widget.colorator != null && widget.colorator!(item.value) != null 187 | ? widget.colorator!(item.value) 188 | : widget.selectedColor != null 189 | ? widget.selectedColor 190 | : Theme.of(context).primaryColor.withOpacity(0.35), 191 | label: Text( 192 | item.label, 193 | style: _selectedValues.contains(item.value) 194 | ? TextStyle( 195 | color: widget.selectedItemsTextStyle?.color ?? 196 | widget.colorator?.call(item.value) ?? 197 | widget.selectedColor?.withOpacity(1) ?? 198 | Theme.of(context).primaryColor, 199 | fontSize: widget.selectedItemsTextStyle != null 200 | ? widget.selectedItemsTextStyle!.fontSize 201 | : null, 202 | ) 203 | : widget.itemsTextStyle, 204 | ), 205 | selected: item.selected, 206 | onSelected: (checked) { 207 | if (checked) { 208 | item.selected = true; 209 | } else { 210 | item.selected = false; 211 | } 212 | setState(() { 213 | _selectedValues = widget.onItemCheckedChange( 214 | _selectedValues, item.value, checked); 215 | }); 216 | if (widget.onSelectionChanged != null) { 217 | widget.onSelectionChanged!(_selectedValues); 218 | } 219 | }, 220 | ), 221 | ); 222 | } 223 | 224 | @override 225 | Widget build(BuildContext context) { 226 | return Container( 227 | padding: 228 | EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), 229 | child: DraggableScrollableSheet( 230 | initialChildSize: widget.initialChildSize ?? 0.3, 231 | minChildSize: widget.minChildSize ?? 0.3, 232 | maxChildSize: widget.maxChildSize ?? 0.6, 233 | expand: false, 234 | builder: (BuildContext context, ScrollController scrollController) { 235 | return Column( 236 | children: [ 237 | Padding( 238 | padding: const EdgeInsets.all(10), 239 | child: Row( 240 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 241 | children: [ 242 | _showSearch 243 | ? Expanded( 244 | child: Container( 245 | padding: const EdgeInsets.only(left: 10), 246 | child: TextField( 247 | autofocus: true, 248 | style: widget.searchTextStyle, 249 | decoration: InputDecoration( 250 | hintStyle: widget.searchHintStyle, 251 | hintText: widget.searchHint ?? "Search", 252 | focusedBorder: UnderlineInputBorder( 253 | borderSide: BorderSide( 254 | color: widget.selectedColor ?? 255 | Theme.of(context).primaryColor), 256 | ), 257 | ), 258 | onChanged: (val) { 259 | List> filteredList = []; 260 | filteredList = widget.updateSearchQuery( 261 | val, widget.items); 262 | setState(() { 263 | if (widget.separateSelectedItems) { 264 | _items = 265 | widget.separateSelected(filteredList); 266 | } else { 267 | _items = filteredList; 268 | } 269 | }); 270 | }, 271 | ), 272 | ), 273 | ) 274 | : widget.title ?? 275 | Text( 276 | "Select", 277 | style: TextStyle(fontSize: 18), 278 | ), 279 | widget.searchable 280 | ? IconButton( 281 | icon: _showSearch 282 | ? widget.closeSearchIcon ?? Icon(Icons.close) 283 | : widget.searchIcon ?? Icon(Icons.search), 284 | onPressed: () { 285 | setState(() { 286 | _showSearch = !_showSearch; 287 | if (!_showSearch) { 288 | if (widget.separateSelectedItems) { 289 | _items = 290 | widget.separateSelected(widget.items); 291 | } else { 292 | _items = widget.items; 293 | } 294 | } 295 | }); 296 | }, 297 | ) 298 | : Padding( 299 | padding: EdgeInsets.all(15), 300 | ), 301 | ], 302 | ), 303 | ), 304 | Expanded( 305 | child: widget.listType == null || 306 | widget.listType == MultiSelectListType.LIST 307 | ? ListView.builder( 308 | controller: scrollController, 309 | itemCount: _items.length, 310 | itemBuilder: (context, index) { 311 | return _buildListItem(_items[index]); 312 | }, 313 | ) 314 | : SingleChildScrollView( 315 | controller: scrollController, 316 | child: Container( 317 | padding: EdgeInsets.all(10), 318 | child: Wrap( 319 | children: _items.map(_buildChipItem).toList(), 320 | ), 321 | ), 322 | ), 323 | ), 324 | Container( 325 | padding: EdgeInsets.all(2), 326 | child: Row( 327 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 328 | children: [ 329 | Expanded( 330 | child: TextButton( 331 | onPressed: () { 332 | widget.onCancelTap(context, widget.initialValue); 333 | }, 334 | child: widget.cancelText ?? 335 | Text( 336 | "CANCEL", 337 | style: TextStyle( 338 | color: (widget.selectedColor != null && 339 | widget.selectedColor != 340 | Colors.transparent) 341 | ? widget.selectedColor!.withOpacity(1) 342 | : Theme.of(context).primaryColor, 343 | ), 344 | ), 345 | ), 346 | ), 347 | SizedBox(width: 10), 348 | Expanded( 349 | child: TextButton( 350 | onPressed: () { 351 | widget.onConfirmTap( 352 | context, _selectedValues, widget.onConfirm); 353 | }, 354 | child: widget.confirmText ?? 355 | Text( 356 | "OK", 357 | style: TextStyle( 358 | color: (widget.selectedColor != null && 359 | widget.selectedColor != 360 | Colors.transparent) 361 | ? widget.selectedColor!.withOpacity(1) 362 | : Theme.of(context).primaryColor, 363 | ), 364 | ), 365 | ), 366 | ), 367 | ], 368 | ), 369 | ), 370 | ], 371 | ); 372 | }, 373 | ), 374 | ); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /lib/dialog/multi_select_dialog_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart' show IterableExtension; 2 | import 'package:flutter/material.dart'; 3 | import '../util/multi_select_list_type.dart'; 4 | import '../util/multi_select_item.dart'; 5 | import '../chip_display/multi_select_chip_display.dart'; 6 | import 'mult_select_dialog.dart'; 7 | 8 | /// A customizable InkWell widget that opens the MultiSelectDialog 9 | // ignore: must_be_immutable 10 | class MultiSelectDialogField extends FormField> { 11 | /// An enum that determines which type of list to render. 12 | final MultiSelectListType? listType; 13 | 14 | /// Style the Container that makes up the field. 15 | final BoxDecoration? decoration; 16 | 17 | /// Set text that is displayed on the button. 18 | final Text? buttonText; 19 | 20 | /// Specify the button icon. 21 | final Icon? buttonIcon; 22 | 23 | /// The text at the top of the dialog. 24 | final Widget? title; 25 | 26 | /// List of items to select from. 27 | final List> items; 28 | 29 | /// Fires when the an item is selected / unselected. 30 | final void Function(List)? onSelectionChanged; 31 | 32 | /// Overrides the default MultiSelectChipDisplay attached to this field. 33 | /// If you want to remove it, use MultiSelectChipDisplay.none(). 34 | final MultiSelectChipDisplay? chipDisplay; 35 | 36 | /// The list of selected values before interaction. 37 | final List initialValue; 38 | 39 | /// Fires when confirm is tapped. 40 | final void Function(List) onConfirm; 41 | 42 | /// Toggles search functionality. 43 | final bool searchable; 44 | 45 | /// Text on the confirm button. 46 | final Text? confirmText; 47 | 48 | /// Text on the cancel button. 49 | final Text? cancelText; 50 | 51 | /// Set the color of the space outside the BottomSheet. 52 | final Color? barrierColor; 53 | 54 | /// Sets the color of the checkbox or chip when it's selected. 55 | final Color? selectedColor; 56 | 57 | /// Sets a fixed height on the dialog. 58 | final double? dialogHeight; 59 | 60 | /// Sets a fixed width on the dialog. 61 | final double? dialogWidth; 62 | 63 | /// Set the placeholder text of the search field. 64 | final String? searchHint; 65 | 66 | /// A function that sets the color of selected items based on their value. 67 | /// It will either set the chip color, or the checkbox color depending on the list type. 68 | final Color Function(V)? colorator; 69 | 70 | /// Set the background color of the dialog. 71 | final Color? backgroundColor; 72 | 73 | /// Color of the chip body or checkbox border while not selected. 74 | final Color? unselectedColor; 75 | 76 | /// Replaces the deafult search icon when searchable is true. 77 | final Icon? searchIcon; 78 | 79 | /// Replaces the default close search icon when searchable is true. 80 | final Icon? closeSearchIcon; 81 | 82 | /// Style the text on the chips or list tiles. 83 | final TextStyle? itemsTextStyle; 84 | 85 | /// Style the text on the selected chips or list tiles. 86 | final TextStyle? selectedItemsTextStyle; 87 | 88 | /// Style the text that is typed into the search field. 89 | final TextStyle? searchTextStyle; 90 | 91 | /// Style the search hint. 92 | final TextStyle? searchHintStyle; 93 | 94 | /// Moves the selected items to the top of the list. 95 | final bool separateSelectedItems; 96 | 97 | /// Set the color of the check in the checkbox 98 | final Color? checkColor; 99 | 100 | /// Whether the user can dismiss the widget by tapping outside 101 | final bool isDismissible; 102 | 103 | final AutovalidateMode autovalidateMode; 104 | final FormFieldValidator>? validator; 105 | final FormFieldSetter>? onSaved; 106 | final GlobalKey? key; 107 | FormFieldState>? state; 108 | 109 | MultiSelectDialogField({ 110 | required this.items, 111 | required this.onConfirm, 112 | this.title, 113 | this.buttonText, 114 | this.buttonIcon, 115 | this.listType, 116 | this.decoration, 117 | this.onSelectionChanged, 118 | this.chipDisplay, 119 | this.searchable = false, 120 | this.confirmText, 121 | this.cancelText, 122 | this.barrierColor, 123 | this.selectedColor, 124 | this.searchHint, 125 | this.dialogHeight, 126 | this.dialogWidth, 127 | this.colorator, 128 | this.backgroundColor, 129 | this.unselectedColor, 130 | this.searchIcon, 131 | this.closeSearchIcon, 132 | this.itemsTextStyle, 133 | this.searchTextStyle, 134 | this.searchHintStyle, 135 | this.selectedItemsTextStyle, 136 | this.separateSelectedItems = false, 137 | this.checkColor, 138 | this.isDismissible = true, 139 | this.onSaved, 140 | this.validator, 141 | this.initialValue = const [], 142 | this.autovalidateMode = AutovalidateMode.disabled, 143 | this.key, 144 | }) : super( 145 | key: key, 146 | onSaved: onSaved, 147 | validator: validator, 148 | autovalidateMode: autovalidateMode, 149 | initialValue: initialValue, 150 | builder: (FormFieldState> state) { 151 | _MultiSelectDialogFieldView field = 152 | _MultiSelectDialogFieldView( 153 | title: title, 154 | items: items, 155 | buttonText: buttonText, 156 | buttonIcon: buttonIcon, 157 | chipDisplay: chipDisplay, 158 | decoration: decoration, 159 | listType: listType, 160 | onConfirm: onConfirm, 161 | onSelectionChanged: onSelectionChanged, 162 | initialValue: initialValue, 163 | searchable: searchable, 164 | confirmText: confirmText, 165 | cancelText: cancelText, 166 | barrierColor: barrierColor, 167 | selectedColor: selectedColor, 168 | searchHint: searchHint, 169 | dialogHeight: dialogHeight, 170 | dialogWidth: dialogWidth, 171 | colorator: colorator, 172 | backgroundColor: backgroundColor, 173 | unselectedColor: unselectedColor, 174 | searchIcon: searchIcon, 175 | closeSearchIcon: closeSearchIcon, 176 | itemsTextStyle: itemsTextStyle, 177 | searchTextStyle: searchTextStyle, 178 | searchHintStyle: searchHintStyle, 179 | selectedItemsTextStyle: selectedItemsTextStyle, 180 | separateSelectedItems: separateSelectedItems, 181 | checkColor: checkColor, 182 | isDismissible: isDismissible, 183 | ); 184 | return _MultiSelectDialogFieldView._withState(field, state); 185 | }); 186 | } 187 | 188 | // ignore: must_be_immutable 189 | class _MultiSelectDialogFieldView extends StatefulWidget { 190 | final MultiSelectListType? listType; 191 | final BoxDecoration? decoration; 192 | final Text? buttonText; 193 | final Icon? buttonIcon; 194 | final Widget? title; 195 | final List> items; 196 | final void Function(List)? onSelectionChanged; 197 | final MultiSelectChipDisplay? chipDisplay; 198 | final List initialValue; 199 | final void Function(List)? onConfirm; 200 | final bool? searchable; 201 | final Text? confirmText; 202 | final Text? cancelText; 203 | final Color? barrierColor; 204 | final Color? selectedColor; 205 | final double? dialogHeight; 206 | final double? dialogWidth; 207 | final String? searchHint; 208 | final Color Function(V)? colorator; 209 | final Color? backgroundColor; 210 | final Color? unselectedColor; 211 | final Icon? searchIcon; 212 | final Icon? closeSearchIcon; 213 | final TextStyle? itemsTextStyle; 214 | final TextStyle? selectedItemsTextStyle; 215 | final TextStyle? searchTextStyle; 216 | final TextStyle? searchHintStyle; 217 | final bool separateSelectedItems; 218 | final Color? checkColor; 219 | final bool isDismissible; 220 | FormFieldState>? state; 221 | 222 | _MultiSelectDialogFieldView({ 223 | required this.items, 224 | this.title, 225 | this.buttonText, 226 | this.buttonIcon, 227 | this.listType, 228 | this.decoration, 229 | this.onSelectionChanged, 230 | this.onConfirm, 231 | this.chipDisplay, 232 | this.initialValue = const [], 233 | this.searchable, 234 | this.confirmText, 235 | this.cancelText, 236 | this.barrierColor, 237 | this.selectedColor, 238 | this.searchHint, 239 | this.dialogHeight, 240 | this.dialogWidth, 241 | this.colorator, 242 | this.backgroundColor, 243 | this.unselectedColor, 244 | this.searchIcon, 245 | this.closeSearchIcon, 246 | this.itemsTextStyle, 247 | this.searchTextStyle, 248 | this.searchHintStyle, 249 | this.selectedItemsTextStyle, 250 | this.separateSelectedItems = false, 251 | this.checkColor, 252 | required this.isDismissible, 253 | }); 254 | 255 | /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectDialogField. 256 | _MultiSelectDialogFieldView._withState( 257 | _MultiSelectDialogFieldView field, FormFieldState> state) 258 | : items = field.items, 259 | title = field.title, 260 | buttonText = field.buttonText, 261 | buttonIcon = field.buttonIcon, 262 | listType = field.listType, 263 | decoration = field.decoration, 264 | onSelectionChanged = field.onSelectionChanged, 265 | onConfirm = field.onConfirm, 266 | chipDisplay = field.chipDisplay, 267 | initialValue = field.initialValue, 268 | searchable = field.searchable, 269 | confirmText = field.confirmText, 270 | cancelText = field.cancelText, 271 | barrierColor = field.barrierColor, 272 | selectedColor = field.selectedColor, 273 | dialogHeight = field.dialogHeight, 274 | dialogWidth = field.dialogWidth, 275 | searchHint = field.searchHint, 276 | colorator = field.colorator, 277 | backgroundColor = field.backgroundColor, 278 | unselectedColor = field.unselectedColor, 279 | searchIcon = field.searchIcon, 280 | closeSearchIcon = field.closeSearchIcon, 281 | itemsTextStyle = field.itemsTextStyle, 282 | searchHintStyle = field.searchHintStyle, 283 | searchTextStyle = field.searchTextStyle, 284 | selectedItemsTextStyle = field.selectedItemsTextStyle, 285 | separateSelectedItems = field.separateSelectedItems, 286 | checkColor = field.checkColor, 287 | isDismissible = field.isDismissible, 288 | state = state; 289 | 290 | @override 291 | __MultiSelectDialogFieldViewState createState() => 292 | __MultiSelectDialogFieldViewState(); 293 | } 294 | 295 | class __MultiSelectDialogFieldViewState 296 | extends State<_MultiSelectDialogFieldView> { 297 | List _selectedItems = []; 298 | 299 | @override 300 | void initState() { 301 | super.initState(); 302 | _selectedItems.addAll(widget.initialValue); 303 | } 304 | 305 | @override 306 | void didUpdateWidget(_MultiSelectDialogFieldView oldWidget) { 307 | super.didUpdateWidget(oldWidget); 308 | 309 | if (oldWidget.initialValue != widget.initialValue) { 310 | _selectedItems = []; 311 | _selectedItems.addAll(widget.initialValue); 312 | 313 | WidgetsBinding.instance.addPostFrameCallback((_) { 314 | widget.state!.didChange(_selectedItems); 315 | }); 316 | } 317 | } 318 | 319 | Widget _buildInheritedChipDisplay() { 320 | List?> chipDisplayItems = []; 321 | chipDisplayItems = _selectedItems 322 | .map((e) => 323 | widget.items.firstWhereOrNull((element) => e == element.value)) 324 | .toList(); 325 | chipDisplayItems.removeWhere((element) => element == null); 326 | if (widget.chipDisplay != null) { 327 | // if user has specified a chipDisplay, use its params 328 | if (widget.chipDisplay!.disabled!) { 329 | return Container(); 330 | } else { 331 | return MultiSelectChipDisplay( 332 | items: chipDisplayItems, 333 | colorator: widget.chipDisplay!.colorator ?? widget.colorator, 334 | onTap: (item) { 335 | List? newValues; 336 | if (widget.chipDisplay!.onTap != null) { 337 | dynamic result = widget.chipDisplay!.onTap!(item); 338 | if (result is List) newValues = result; 339 | } 340 | if (newValues != null) { 341 | _selectedItems = newValues; 342 | if (widget.state != null) { 343 | widget.state!.didChange(_selectedItems); 344 | } 345 | } 346 | }, 347 | decoration: widget.chipDisplay!.decoration, 348 | chipColor: widget.chipDisplay!.chipColor ?? 349 | ((widget.selectedColor != null && 350 | widget.selectedColor != Colors.transparent) 351 | ? widget.selectedColor!.withOpacity(0.35) 352 | : null), 353 | alignment: widget.chipDisplay!.alignment, 354 | textStyle: widget.chipDisplay!.textStyle, 355 | icon: widget.chipDisplay!.icon, 356 | shape: widget.chipDisplay!.shape, 357 | scroll: widget.chipDisplay!.scroll, 358 | scrollBar: widget.chipDisplay!.scrollBar, 359 | height: widget.chipDisplay!.height, 360 | chipWidth: widget.chipDisplay!.chipWidth, 361 | ); 362 | } 363 | } else { 364 | // user didn't specify a chipDisplay, build the default 365 | return MultiSelectChipDisplay( 366 | items: chipDisplayItems, 367 | colorator: widget.colorator, 368 | chipColor: (widget.selectedColor != null && 369 | widget.selectedColor != Colors.transparent) 370 | ? widget.selectedColor!.withOpacity(0.35) 371 | : null, 372 | ); 373 | } 374 | } 375 | 376 | /// Calls showDialog() and renders a MultiSelectDialog. 377 | _showDialog(BuildContext ctx) async { 378 | await showDialog( 379 | barrierColor: widget.barrierColor, 380 | barrierDismissible: widget.isDismissible, 381 | context: context, 382 | builder: (ctx) { 383 | return MultiSelectDialog( 384 | checkColor: widget.checkColor, 385 | selectedItemsTextStyle: widget.selectedItemsTextStyle, 386 | searchHintStyle: widget.searchHintStyle, 387 | searchTextStyle: widget.searchTextStyle, 388 | itemsTextStyle: widget.itemsTextStyle, 389 | searchIcon: widget.searchIcon, 390 | closeSearchIcon: widget.closeSearchIcon, 391 | unselectedColor: widget.unselectedColor, 392 | backgroundColor: widget.backgroundColor, 393 | colorator: widget.colorator, 394 | searchHint: widget.searchHint, 395 | selectedColor: widget.selectedColor, 396 | onSelectionChanged: widget.onSelectionChanged, 397 | height: widget.dialogHeight, 398 | width: widget.dialogWidth, 399 | listType: widget.listType, 400 | items: widget.items, 401 | title: widget.title ?? const Text("Select"), 402 | initialValue: _selectedItems, 403 | searchable: widget.searchable ?? false, 404 | confirmText: widget.confirmText, 405 | cancelText: widget.cancelText, 406 | separateSelectedItems: widget.separateSelectedItems, 407 | onConfirm: (selected) { 408 | _selectedItems = selected; 409 | if (widget.state != null) { 410 | widget.state!.didChange(_selectedItems); 411 | } 412 | if (widget.onConfirm != null) widget.onConfirm!(_selectedItems); 413 | }, 414 | ); 415 | }, 416 | ); 417 | } 418 | 419 | @override 420 | Widget build(BuildContext context) { 421 | return Column( 422 | mainAxisAlignment: MainAxisAlignment.start, 423 | children: [ 424 | InkWell( 425 | onTap: () { 426 | _showDialog(context); 427 | }, 428 | child: Container( 429 | decoration: widget.state != null 430 | ? widget.decoration ?? 431 | BoxDecoration( 432 | border: Border( 433 | bottom: BorderSide( 434 | color: widget.state != null && widget.state!.hasError 435 | ? Colors.red.shade800.withOpacity(0.6) 436 | : _selectedItems.isNotEmpty 437 | ? (widget.selectedColor != null && 438 | widget.selectedColor != 439 | Colors.transparent) 440 | ? widget.selectedColor! 441 | : Theme.of(context).primaryColor 442 | : Colors.black45, 443 | width: _selectedItems.isNotEmpty 444 | ? (widget.state != null && widget.state!.hasError) 445 | ? 1.4 446 | : 1.8 447 | : 1.2, 448 | ), 449 | ), 450 | ) 451 | : widget.decoration, 452 | padding: const EdgeInsets.all(10), 453 | child: Row( 454 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 455 | children: [ 456 | widget.buttonText ?? const Text("Select"), 457 | widget.buttonIcon ?? const Icon(Icons.arrow_downward), 458 | ], 459 | ), 460 | ), 461 | ), 462 | _buildInheritedChipDisplay(), 463 | widget.state != null && widget.state!.hasError 464 | ? const SizedBox(height: 5) 465 | : Container(), 466 | widget.state != null && widget.state!.hasError 467 | ? Row( 468 | children: [ 469 | Padding( 470 | padding: const EdgeInsets.only(left: 4), 471 | child: Text( 472 | widget.state!.errorText!, 473 | style: TextStyle( 474 | color: Colors.red[800], 475 | fontSize: 12.5, 476 | ), 477 | ), 478 | ), 479 | ], 480 | ) 481 | : Container(), 482 | ], 483 | ); 484 | } 485 | } 486 | -------------------------------------------------------------------------------- /lib/bottom_sheet/multi_select_bottom_sheet_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:collection/collection.dart' show IterableExtension; 2 | import 'package:flutter/material.dart'; 3 | import '../util/multi_select_list_type.dart'; 4 | import '../chip_display/multi_select_chip_display.dart'; 5 | import '../util/multi_select_item.dart'; 6 | import 'multi_select_bottom_sheet.dart'; 7 | 8 | /// A customizable InkWell widget that opens the MultiSelectBottomSheet 9 | // ignore: must_be_immutable 10 | class MultiSelectBottomSheetField extends FormField> { 11 | /// Style the Container that makes up the field. 12 | final BoxDecoration? decoration; 13 | 14 | /// Set text that is displayed on the button. 15 | final Text? buttonText; 16 | 17 | /// Specify the button icon. 18 | final Icon? buttonIcon; 19 | 20 | /// List of items to select from. 21 | final List> items; 22 | 23 | /// The list of selected values before interaction. 24 | final List initialValue; 25 | 26 | /// The text at the top of the dialog. 27 | final Widget? title; 28 | 29 | /// Fires when the an item is selected / unselected. 30 | final void Function(List)? onSelectionChanged; 31 | 32 | /// Fires when confirm is tapped. 33 | final void Function(List) onConfirm; 34 | 35 | /// Toggles search functionality. 36 | final bool searchable; 37 | 38 | /// Text on the confirm button. 39 | final Text? confirmText; 40 | 41 | /// Text on the cancel button. 42 | final Text? cancelText; 43 | 44 | /// An enum that determines which type of list to render. 45 | final MultiSelectListType? listType; 46 | 47 | /// Sets the color of the checkbox or chip body when selected. 48 | final Color? selectedColor; 49 | 50 | /// Set the hint text of the search field. 51 | final String? searchHint; 52 | 53 | /// Set the initial height of the BottomSheet. 54 | final double? initialChildSize; 55 | 56 | /// Set the minimum height threshold of the BottomSheet before it closes. 57 | final double? minChildSize; 58 | 59 | /// Set the maximum height of the BottomSheet. 60 | final double? maxChildSize; 61 | 62 | /// Apply a ShapeBorder to alter the edges of the BottomSheet. 63 | final ShapeBorder? shape; 64 | 65 | /// Set the color of the space outside the BottomSheet. 66 | final Color? barrierColor; 67 | 68 | /// Overrides the default MultiSelectChipDisplay attached to this field. 69 | /// If you want to remove it, use MultiSelectChipDisplay.none(). 70 | final MultiSelectChipDisplay? chipDisplay; 71 | 72 | /// A function that sets the color of selected items based on their value. 73 | /// It will either set the chip color, or the checkbox color depending on the list type. 74 | final Color Function(V)? colorator; 75 | 76 | /// Set the background color of the bottom sheet. 77 | final Color? backgroundColor; 78 | 79 | /// Color of the chip body or checkbox border while not selected. 80 | final Color? unselectedColor; 81 | 82 | /// Replaces the deafult search icon when searchable is true. 83 | final Icon? searchIcon; 84 | 85 | /// Replaces the default close search icon when searchable is true. 86 | final Icon? closeSearchIcon; 87 | 88 | /// The TextStyle of the items within the BottomSheet. 89 | final TextStyle? itemsTextStyle; 90 | 91 | /// Style the text on the selected chips or list tiles. 92 | final TextStyle? selectedItemsTextStyle; 93 | 94 | /// Moves the selected items to the top of the list. 95 | final bool separateSelectedItems; 96 | 97 | /// Style the text that is typed into the search field. 98 | final TextStyle? searchTextStyle; 99 | 100 | /// Style the search hint. 101 | final TextStyle? searchHintStyle; 102 | 103 | /// Set the color of the check in the checkbox 104 | final Color? checkColor; 105 | 106 | /// Whether the user can dismiss the widget by tapping outside 107 | final bool isDismissible; 108 | 109 | final AutovalidateMode autovalidateMode; 110 | final FormFieldValidator>? validator; 111 | final FormFieldSetter>? onSaved; 112 | final GlobalKey? key; 113 | FormFieldState>? state; 114 | 115 | MultiSelectBottomSheetField({ 116 | required this.items, 117 | required this.onConfirm, 118 | this.title, 119 | this.buttonText, 120 | this.buttonIcon, 121 | this.listType, 122 | this.decoration, 123 | this.onSelectionChanged, 124 | this.chipDisplay, 125 | this.initialValue = const [], 126 | this.searchable = false, 127 | this.confirmText, 128 | this.cancelText, 129 | this.selectedColor, 130 | this.initialChildSize, 131 | this.minChildSize, 132 | this.maxChildSize, 133 | this.shape, 134 | this.barrierColor, 135 | this.searchHint, 136 | this.colorator, 137 | this.backgroundColor, 138 | this.unselectedColor, 139 | this.searchIcon, 140 | this.closeSearchIcon, 141 | this.itemsTextStyle, 142 | this.searchTextStyle, 143 | this.searchHintStyle, 144 | this.selectedItemsTextStyle, 145 | this.separateSelectedItems = false, 146 | this.checkColor, 147 | this.isDismissible = true, 148 | this.key, 149 | this.onSaved, 150 | this.validator, 151 | this.autovalidateMode = AutovalidateMode.disabled, 152 | }) : super( 153 | key: key, 154 | onSaved: onSaved, 155 | validator: validator, 156 | autovalidateMode: autovalidateMode, 157 | initialValue: initialValue, 158 | builder: (FormFieldState> state) { 159 | _MultiSelectBottomSheetFieldView view = 160 | _MultiSelectBottomSheetFieldView( 161 | items: items, 162 | decoration: decoration, 163 | unselectedColor: unselectedColor, 164 | colorator: colorator, 165 | itemsTextStyle: itemsTextStyle, 166 | selectedItemsTextStyle: selectedItemsTextStyle, 167 | backgroundColor: backgroundColor, 168 | title: title, 169 | initialValue: initialValue, 170 | barrierColor: barrierColor, 171 | buttonIcon: buttonIcon, 172 | buttonText: buttonText, 173 | cancelText: cancelText, 174 | chipDisplay: chipDisplay, 175 | closeSearchIcon: closeSearchIcon, 176 | confirmText: confirmText, 177 | initialChildSize: initialChildSize, 178 | listType: listType, 179 | maxChildSize: maxChildSize, 180 | minChildSize: minChildSize, 181 | onConfirm: onConfirm, 182 | onSelectionChanged: onSelectionChanged, 183 | searchHintStyle: searchHintStyle, 184 | searchIcon: searchIcon, 185 | searchHint: searchHint, 186 | searchTextStyle: searchTextStyle, 187 | searchable: searchable, 188 | selectedColor: selectedColor, 189 | separateSelectedItems: separateSelectedItems, 190 | shape: shape, 191 | checkColor: checkColor, 192 | isDismissible: isDismissible, 193 | ); 194 | return _MultiSelectBottomSheetFieldView._withState( 195 | view as _MultiSelectBottomSheetFieldView, state); 196 | }); 197 | } 198 | 199 | // ignore: must_be_immutable 200 | class _MultiSelectBottomSheetFieldView extends StatefulWidget { 201 | final BoxDecoration? decoration; 202 | final Text? buttonText; 203 | final Icon? buttonIcon; 204 | final List> items; 205 | final List initialValue; 206 | final Widget? title; 207 | final void Function(List)? onSelectionChanged; 208 | final void Function(List)? onConfirm; 209 | final bool searchable; 210 | final Text? confirmText; 211 | final Text? cancelText; 212 | final MultiSelectListType? listType; 213 | final Color? selectedColor; 214 | final String? searchHint; 215 | final double? initialChildSize; 216 | final double? minChildSize; 217 | final double? maxChildSize; 218 | final ShapeBorder? shape; 219 | final Color? barrierColor; 220 | final MultiSelectChipDisplay? chipDisplay; 221 | final Color Function(V)? colorator; 222 | final Color? backgroundColor; 223 | final Color? unselectedColor; 224 | final Icon? searchIcon; 225 | final Icon? closeSearchIcon; 226 | final TextStyle? itemsTextStyle; 227 | final TextStyle? selectedItemsTextStyle; 228 | final TextStyle? searchTextStyle; 229 | final TextStyle? searchHintStyle; 230 | final bool separateSelectedItems; 231 | final Color? checkColor; 232 | final bool isDismissible; 233 | FormFieldState>? state; 234 | 235 | _MultiSelectBottomSheetFieldView({ 236 | required this.items, 237 | this.title, 238 | this.buttonText, 239 | this.buttonIcon, 240 | this.listType, 241 | this.decoration, 242 | this.onSelectionChanged, 243 | this.onConfirm, 244 | this.chipDisplay, 245 | required this.initialValue, 246 | required this.searchable, 247 | this.confirmText, 248 | this.cancelText, 249 | this.selectedColor, 250 | this.initialChildSize, 251 | this.minChildSize, 252 | this.maxChildSize, 253 | this.shape, 254 | this.barrierColor, 255 | this.searchHint, 256 | this.colorator, 257 | this.backgroundColor, 258 | this.unselectedColor, 259 | this.searchIcon, 260 | this.closeSearchIcon, 261 | this.itemsTextStyle, 262 | this.searchTextStyle, 263 | this.searchHintStyle, 264 | this.selectedItemsTextStyle, 265 | required this.separateSelectedItems, 266 | this.checkColor, 267 | required this.isDismissible, 268 | }); 269 | 270 | /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectBottomSheetField. 271 | _MultiSelectBottomSheetFieldView._withState( 272 | _MultiSelectBottomSheetFieldView field, FormFieldState> state) 273 | : items = field.items, 274 | title = field.title, 275 | buttonText = field.buttonText, 276 | buttonIcon = field.buttonIcon, 277 | listType = field.listType, 278 | decoration = field.decoration, 279 | onSelectionChanged = field.onSelectionChanged, 280 | onConfirm = field.onConfirm, 281 | chipDisplay = field.chipDisplay, 282 | initialValue = field.initialValue, 283 | searchable = field.searchable, 284 | confirmText = field.confirmText, 285 | cancelText = field.cancelText, 286 | selectedColor = field.selectedColor, 287 | initialChildSize = field.initialChildSize, 288 | minChildSize = field.minChildSize, 289 | maxChildSize = field.maxChildSize, 290 | shape = field.shape, 291 | barrierColor = field.barrierColor, 292 | searchHint = field.searchHint, 293 | colorator = field.colorator, 294 | backgroundColor = field.backgroundColor, 295 | unselectedColor = field.unselectedColor, 296 | searchIcon = field.searchIcon, 297 | closeSearchIcon = field.closeSearchIcon, 298 | itemsTextStyle = field.itemsTextStyle, 299 | searchHintStyle = field.searchHintStyle, 300 | searchTextStyle = field.searchTextStyle, 301 | selectedItemsTextStyle = field.selectedItemsTextStyle, 302 | separateSelectedItems = field.separateSelectedItems, 303 | checkColor = field.checkColor, 304 | isDismissible = field.isDismissible, 305 | state = state; 306 | 307 | @override 308 | __MultiSelectBottomSheetFieldViewState createState() => 309 | __MultiSelectBottomSheetFieldViewState(); 310 | } 311 | 312 | class __MultiSelectBottomSheetFieldViewState 313 | extends State<_MultiSelectBottomSheetFieldView> { 314 | List _selectedItems = []; 315 | 316 | @override 317 | void initState() { 318 | super.initState(); 319 | _selectedItems.addAll(widget.initialValue); 320 | } 321 | 322 | @override 323 | void didUpdateWidget(_MultiSelectBottomSheetFieldView oldWidget) { 324 | super.didUpdateWidget(oldWidget); 325 | 326 | if (oldWidget.initialValue != widget.initialValue) { 327 | _selectedItems = []; 328 | _selectedItems.addAll(widget.initialValue); 329 | 330 | WidgetsBinding.instance.addPostFrameCallback((_) { 331 | widget.state!.didChange(_selectedItems); 332 | }); 333 | } 334 | } 335 | 336 | Widget _buildInheritedChipDisplay() { 337 | List?> chipDisplayItems = []; 338 | chipDisplayItems = _selectedItems 339 | .map((e) => 340 | widget.items.firstWhereOrNull((element) => e == element.value)) 341 | .toList(); 342 | chipDisplayItems.removeWhere((element) => element == null); 343 | if (widget.chipDisplay != null) { 344 | // if user has specified a chipDisplay, use its params 345 | if (widget.chipDisplay!.disabled!) { 346 | return Container(); 347 | } else { 348 | return MultiSelectChipDisplay( 349 | items: chipDisplayItems, 350 | colorator: widget.chipDisplay!.colorator ?? widget.colorator, 351 | onTap: (item) { 352 | List? newValues; 353 | if (widget.chipDisplay!.onTap != null) { 354 | dynamic result = widget.chipDisplay!.onTap!(item); 355 | if (result is List) newValues = result; 356 | } 357 | if (newValues != null) { 358 | _selectedItems = newValues; 359 | if (widget.state != null) { 360 | widget.state!.didChange(_selectedItems); 361 | } 362 | } 363 | }, 364 | decoration: widget.chipDisplay!.decoration, 365 | chipColor: widget.chipDisplay!.chipColor ?? 366 | ((widget.selectedColor != null && 367 | widget.selectedColor != Colors.transparent) 368 | ? widget.selectedColor!.withOpacity(0.35) 369 | : null), 370 | alignment: widget.chipDisplay!.alignment, 371 | textStyle: widget.chipDisplay!.textStyle, 372 | icon: widget.chipDisplay!.icon, 373 | shape: widget.chipDisplay!.shape, 374 | scroll: widget.chipDisplay!.scroll, 375 | scrollBar: widget.chipDisplay!.scrollBar, 376 | height: widget.chipDisplay!.height, 377 | chipWidth: widget.chipDisplay!.chipWidth, 378 | ); 379 | } 380 | } else { 381 | // user didn't specify a chipDisplay, build the default 382 | return MultiSelectChipDisplay( 383 | items: chipDisplayItems, 384 | colorator: widget.colorator, 385 | chipColor: (widget.selectedColor != null && 386 | widget.selectedColor != Colors.transparent) 387 | ? widget.selectedColor!.withOpacity(0.35) 388 | : null, 389 | ); 390 | } 391 | } 392 | 393 | _showBottomSheet(BuildContext ctx) async { 394 | List? myVar = await showModalBottomSheet>( 395 | isDismissible: widget.isDismissible, 396 | backgroundColor: widget.backgroundColor, 397 | barrierColor: widget.barrierColor, 398 | shape: widget.shape ?? 399 | RoundedRectangleBorder( 400 | borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)), 401 | ), 402 | isScrollControlled: true, 403 | context: context, 404 | builder: (context) { 405 | return MultiSelectBottomSheet( 406 | checkColor: widget.checkColor, 407 | selectedItemsTextStyle: widget.selectedItemsTextStyle, 408 | searchTextStyle: widget.searchTextStyle, 409 | searchHintStyle: widget.searchHintStyle, 410 | itemsTextStyle: widget.itemsTextStyle, 411 | searchIcon: widget.searchIcon, 412 | closeSearchIcon: widget.closeSearchIcon, 413 | unselectedColor: widget.unselectedColor, 414 | colorator: widget.colorator, 415 | searchHint: widget.searchHint, 416 | selectedColor: widget.selectedColor, 417 | listType: widget.listType, 418 | items: widget.items, 419 | cancelText: widget.cancelText, 420 | confirmText: widget.confirmText, 421 | separateSelectedItems: widget.separateSelectedItems, 422 | initialValue: _selectedItems, 423 | onConfirm: (selected) { 424 | if (widget.state != null) { 425 | widget.state!.didChange(selected); 426 | } 427 | _selectedItems = selected; 428 | if (widget.onConfirm != null) widget.onConfirm!(selected); 429 | }, 430 | onSelectionChanged: widget.onSelectionChanged, 431 | searchable: widget.searchable, 432 | title: widget.title, 433 | initialChildSize: widget.initialChildSize, 434 | minChildSize: widget.minChildSize, 435 | maxChildSize: widget.maxChildSize, 436 | ); 437 | }); 438 | print(myVar.toString()); 439 | _selectedItems = myVar!; 440 | } 441 | 442 | @override 443 | Widget build(BuildContext context) { 444 | return Column( 445 | mainAxisAlignment: MainAxisAlignment.start, 446 | children: [ 447 | InkWell( 448 | onTap: () { 449 | _showBottomSheet(context); 450 | }, 451 | child: Container( 452 | decoration: widget.state != null 453 | ? widget.decoration ?? 454 | BoxDecoration( 455 | border: Border( 456 | bottom: BorderSide( 457 | color: widget.state != null && widget.state!.hasError 458 | ? Colors.red.shade800.withOpacity(0.6) 459 | : _selectedItems.isNotEmpty 460 | ? (widget.selectedColor != null && 461 | widget.selectedColor != 462 | Colors.transparent) 463 | ? widget.selectedColor! 464 | : Theme.of(context).primaryColor 465 | : Colors.black45, 466 | width: _selectedItems.isNotEmpty 467 | ? (widget.state != null && widget.state!.hasError) 468 | ? 1.4 469 | : 1.8 470 | : 1.2, 471 | ), 472 | ), 473 | ) 474 | : widget.decoration, 475 | padding: EdgeInsets.all(10), 476 | child: Row( 477 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 478 | children: [ 479 | widget.buttonText ?? Text("Select"), 480 | widget.buttonIcon ?? Icon(Icons.arrow_downward), 481 | ], 482 | ), 483 | ), 484 | ), 485 | _buildInheritedChipDisplay(), 486 | widget.state != null && widget.state!.hasError 487 | ? SizedBox(height: 5) 488 | : Container(), 489 | widget.state != null && widget.state!.hasError 490 | ? Row( 491 | children: [ 492 | Padding( 493 | padding: const EdgeInsets.only(left: 4), 494 | child: Text( 495 | widget.state!.errorText!, 496 | style: TextStyle( 497 | color: Colors.red[800], 498 | fontSize: 12.5, 499 | ), 500 | ), 501 | ), 502 | ], 503 | ) 504 | : Container(), 505 | ], 506 | ); 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 97C146F11CF9000F007C117D /* Supporting Files */, 94 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 95 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 96 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 97 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 98 | ); 99 | path = Runner; 100 | sourceTree = ""; 101 | }; 102 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | ); 106 | name = "Supporting Files"; 107 | sourceTree = ""; 108 | }; 109 | /* End PBXGroup section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | 97C146ED1CF9000F007C117D /* Runner */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 115 | buildPhases = ( 116 | 9740EEB61CF901F6004384FC /* Run Script */, 117 | 97C146EA1CF9000F007C117D /* Sources */, 118 | 97C146EB1CF9000F007C117D /* Frameworks */, 119 | 97C146EC1CF9000F007C117D /* Resources */, 120 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 121 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 122 | ); 123 | buildRules = ( 124 | ); 125 | dependencies = ( 126 | ); 127 | name = Runner; 128 | productName = Runner; 129 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 130 | productType = "com.apple.product-type.application"; 131 | }; 132 | /* End PBXNativeTarget section */ 133 | 134 | /* Begin PBXProject section */ 135 | 97C146E61CF9000F007C117D /* Project object */ = { 136 | isa = PBXProject; 137 | attributes = { 138 | LastUpgradeCheck = 1020; 139 | ORGANIZATIONNAME = ""; 140 | TargetAttributes = { 141 | 97C146ED1CF9000F007C117D = { 142 | CreatedOnToolsVersion = 7.3.1; 143 | LastSwiftMigration = 1100; 144 | }; 145 | }; 146 | }; 147 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 148 | compatibilityVersion = "Xcode 9.3"; 149 | developmentRegion = en; 150 | hasScannedForEncodings = 0; 151 | knownRegions = ( 152 | en, 153 | Base, 154 | ); 155 | mainGroup = 97C146E51CF9000F007C117D; 156 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 157 | projectDirPath = ""; 158 | projectRoot = ""; 159 | targets = ( 160 | 97C146ED1CF9000F007C117D /* Runner */, 161 | ); 162 | }; 163 | /* End PBXProject section */ 164 | 165 | /* Begin PBXResourcesBuildPhase section */ 166 | 97C146EC1CF9000F007C117D /* Resources */ = { 167 | isa = PBXResourcesBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 171 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 172 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 173 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXResourcesBuildPhase section */ 178 | 179 | /* Begin PBXShellScriptBuildPhase section */ 180 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 181 | isa = PBXShellScriptBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | ); 185 | inputPaths = ( 186 | ); 187 | name = "Thin Binary"; 188 | outputPaths = ( 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | shellPath = /bin/sh; 192 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 193 | }; 194 | 9740EEB61CF901F6004384FC /* Run Script */ = { 195 | isa = PBXShellScriptBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | ); 199 | inputPaths = ( 200 | ); 201 | name = "Run Script"; 202 | outputPaths = ( 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | shellPath = /bin/sh; 206 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 207 | }; 208 | /* End PBXShellScriptBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | 97C146EA1CF9000F007C117D /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 216 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | /* End PBXSourcesBuildPhase section */ 221 | 222 | /* Begin PBXVariantGroup section */ 223 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C146FB1CF9000F007C117D /* Base */, 227 | ); 228 | name = Main.storyboard; 229 | sourceTree = ""; 230 | }; 231 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 232 | isa = PBXVariantGroup; 233 | children = ( 234 | 97C147001CF9000F007C117D /* Base */, 235 | ); 236 | name = LaunchScreen.storyboard; 237 | sourceTree = ""; 238 | }; 239 | /* End PBXVariantGroup section */ 240 | 241 | /* Begin XCBuildConfiguration section */ 242 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 243 | isa = XCBuildConfiguration; 244 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 245 | buildSettings = { 246 | ALWAYS_SEARCH_USER_PATHS = NO; 247 | CLANG_ANALYZER_NONNULL = YES; 248 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 249 | CLANG_CXX_LIBRARY = "libc++"; 250 | CLANG_ENABLE_MODULES = YES; 251 | CLANG_ENABLE_OBJC_ARC = YES; 252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 253 | CLANG_WARN_BOOL_CONVERSION = YES; 254 | CLANG_WARN_COMMA = YES; 255 | CLANG_WARN_CONSTANT_CONVERSION = YES; 256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 258 | CLANG_WARN_EMPTY_BODY = YES; 259 | CLANG_WARN_ENUM_CONVERSION = YES; 260 | CLANG_WARN_INFINITE_RECURSION = YES; 261 | CLANG_WARN_INT_CONVERSION = YES; 262 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 263 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 264 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 265 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 266 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 267 | CLANG_WARN_STRICT_PROTOTYPES = YES; 268 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 272 | COPY_PHASE_STRIP = NO; 273 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 274 | ENABLE_NS_ASSERTIONS = NO; 275 | ENABLE_STRICT_OBJC_MSGSEND = YES; 276 | GCC_C_LANGUAGE_STANDARD = gnu99; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 280 | GCC_WARN_UNDECLARED_SELECTOR = YES; 281 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 282 | GCC_WARN_UNUSED_FUNCTION = YES; 283 | GCC_WARN_UNUSED_VARIABLE = YES; 284 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 285 | MTL_ENABLE_DEBUG_INFO = NO; 286 | SDKROOT = iphoneos; 287 | SUPPORTED_PLATFORMS = iphoneos; 288 | TARGETED_DEVICE_FAMILY = "1,2"; 289 | VALIDATE_PRODUCT = YES; 290 | }; 291 | name = Profile; 292 | }; 293 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 294 | isa = XCBuildConfiguration; 295 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 296 | buildSettings = { 297 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 298 | CLANG_ENABLE_MODULES = YES; 299 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 300 | ENABLE_BITCODE = NO; 301 | FRAMEWORK_SEARCH_PATHS = ( 302 | "$(inherited)", 303 | "$(PROJECT_DIR)/Flutter", 304 | ); 305 | INFOPLIST_FILE = Runner/Info.plist; 306 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 307 | LIBRARY_SEARCH_PATHS = ( 308 | "$(inherited)", 309 | "$(PROJECT_DIR)/Flutter", 310 | ); 311 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 312 | PRODUCT_NAME = "$(TARGET_NAME)"; 313 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 314 | SWIFT_VERSION = 5.0; 315 | VERSIONING_SYSTEM = "apple-generic"; 316 | }; 317 | name = Profile; 318 | }; 319 | 97C147031CF9000F007C117D /* Debug */ = { 320 | isa = XCBuildConfiguration; 321 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 322 | buildSettings = { 323 | ALWAYS_SEARCH_USER_PATHS = NO; 324 | CLANG_ANALYZER_NONNULL = YES; 325 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 326 | CLANG_CXX_LIBRARY = "libc++"; 327 | CLANG_ENABLE_MODULES = YES; 328 | CLANG_ENABLE_OBJC_ARC = YES; 329 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 330 | CLANG_WARN_BOOL_CONVERSION = YES; 331 | CLANG_WARN_COMMA = YES; 332 | CLANG_WARN_CONSTANT_CONVERSION = YES; 333 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_INFINITE_RECURSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 341 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 343 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 344 | CLANG_WARN_STRICT_PROTOTYPES = YES; 345 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 346 | CLANG_WARN_UNREACHABLE_CODE = YES; 347 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 348 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 349 | COPY_PHASE_STRIP = NO; 350 | DEBUG_INFORMATION_FORMAT = dwarf; 351 | ENABLE_STRICT_OBJC_MSGSEND = YES; 352 | ENABLE_TESTABILITY = YES; 353 | GCC_C_LANGUAGE_STANDARD = gnu99; 354 | GCC_DYNAMIC_NO_PIC = NO; 355 | GCC_NO_COMMON_BLOCKS = YES; 356 | GCC_OPTIMIZATION_LEVEL = 0; 357 | GCC_PREPROCESSOR_DEFINITIONS = ( 358 | "DEBUG=1", 359 | "$(inherited)", 360 | ); 361 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 362 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 363 | GCC_WARN_UNDECLARED_SELECTOR = YES; 364 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 365 | GCC_WARN_UNUSED_FUNCTION = YES; 366 | GCC_WARN_UNUSED_VARIABLE = YES; 367 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 368 | MTL_ENABLE_DEBUG_INFO = YES; 369 | ONLY_ACTIVE_ARCH = YES; 370 | SDKROOT = iphoneos; 371 | TARGETED_DEVICE_FAMILY = "1,2"; 372 | }; 373 | name = Debug; 374 | }; 375 | 97C147041CF9000F007C117D /* Release */ = { 376 | isa = XCBuildConfiguration; 377 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_ANALYZER_NONNULL = YES; 381 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 382 | CLANG_CXX_LIBRARY = "libc++"; 383 | CLANG_ENABLE_MODULES = YES; 384 | CLANG_ENABLE_OBJC_ARC = YES; 385 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 386 | CLANG_WARN_BOOL_CONVERSION = YES; 387 | CLANG_WARN_COMMA = YES; 388 | CLANG_WARN_CONSTANT_CONVERSION = YES; 389 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 390 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 391 | CLANG_WARN_EMPTY_BODY = YES; 392 | CLANG_WARN_ENUM_CONVERSION = YES; 393 | CLANG_WARN_INFINITE_RECURSION = YES; 394 | CLANG_WARN_INT_CONVERSION = YES; 395 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 397 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 398 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 399 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 400 | CLANG_WARN_STRICT_PROTOTYPES = YES; 401 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 402 | CLANG_WARN_UNREACHABLE_CODE = YES; 403 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 404 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 405 | COPY_PHASE_STRIP = NO; 406 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 407 | ENABLE_NS_ASSERTIONS = NO; 408 | ENABLE_STRICT_OBJC_MSGSEND = YES; 409 | GCC_C_LANGUAGE_STANDARD = gnu99; 410 | GCC_NO_COMMON_BLOCKS = YES; 411 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 412 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 413 | GCC_WARN_UNDECLARED_SELECTOR = YES; 414 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 415 | GCC_WARN_UNUSED_FUNCTION = YES; 416 | GCC_WARN_UNUSED_VARIABLE = YES; 417 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 418 | MTL_ENABLE_DEBUG_INFO = NO; 419 | SDKROOT = iphoneos; 420 | SUPPORTED_PLATFORMS = iphoneos; 421 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 422 | TARGETED_DEVICE_FAMILY = "1,2"; 423 | VALIDATE_PRODUCT = YES; 424 | }; 425 | name = Release; 426 | }; 427 | 97C147061CF9000F007C117D /* Debug */ = { 428 | isa = XCBuildConfiguration; 429 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | CLANG_ENABLE_MODULES = YES; 433 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 434 | ENABLE_BITCODE = NO; 435 | FRAMEWORK_SEARCH_PATHS = ( 436 | "$(inherited)", 437 | "$(PROJECT_DIR)/Flutter", 438 | ); 439 | INFOPLIST_FILE = Runner/Info.plist; 440 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 441 | LIBRARY_SEARCH_PATHS = ( 442 | "$(inherited)", 443 | "$(PROJECT_DIR)/Flutter", 444 | ); 445 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 446 | PRODUCT_NAME = "$(TARGET_NAME)"; 447 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 448 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 449 | SWIFT_VERSION = 5.0; 450 | VERSIONING_SYSTEM = "apple-generic"; 451 | }; 452 | name = Debug; 453 | }; 454 | 97C147071CF9000F007C117D /* Release */ = { 455 | isa = XCBuildConfiguration; 456 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 457 | buildSettings = { 458 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 459 | CLANG_ENABLE_MODULES = YES; 460 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 461 | ENABLE_BITCODE = NO; 462 | FRAMEWORK_SEARCH_PATHS = ( 463 | "$(inherited)", 464 | "$(PROJECT_DIR)/Flutter", 465 | ); 466 | INFOPLIST_FILE = Runner/Info.plist; 467 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 468 | LIBRARY_SEARCH_PATHS = ( 469 | "$(inherited)", 470 | "$(PROJECT_DIR)/Flutter", 471 | ); 472 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 473 | PRODUCT_NAME = "$(TARGET_NAME)"; 474 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 475 | SWIFT_VERSION = 5.0; 476 | VERSIONING_SYSTEM = "apple-generic"; 477 | }; 478 | name = Release; 479 | }; 480 | /* End XCBuildConfiguration section */ 481 | 482 | /* Begin XCConfigurationList section */ 483 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 484 | isa = XCConfigurationList; 485 | buildConfigurations = ( 486 | 97C147031CF9000F007C117D /* Debug */, 487 | 97C147041CF9000F007C117D /* Release */, 488 | 249021D3217E4FDB00AE95B9 /* Profile */, 489 | ); 490 | defaultConfigurationIsVisible = 0; 491 | defaultConfigurationName = Release; 492 | }; 493 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 494 | isa = XCConfigurationList; 495 | buildConfigurations = ( 496 | 97C147061CF9000F007C117D /* Debug */, 497 | 97C147071CF9000F007C117D /* Release */, 498 | 249021D4217E4FDB00AE95B9 /* Profile */, 499 | ); 500 | defaultConfigurationIsVisible = 0; 501 | defaultConfigurationName = Release; 502 | }; 503 | /* End XCConfigurationList section */ 504 | }; 505 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 506 | } 507 | -------------------------------------------------------------------------------- /lib/chip_field/multi_select_chip_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../multi_select_flutter.dart'; 3 | 4 | class MultiSelectChipField extends FormField> { 5 | /// Style the Container that makes up the field. 6 | final BoxDecoration? decoration; 7 | 8 | /// List of items to select from. 9 | final List> items; 10 | 11 | /// Color of the chip while not selected. 12 | final Color? chipColor; 13 | 14 | /// Sets the color of the chip while selected. 15 | final Color? selectedChipColor; 16 | 17 | /// Style the text of the chips. 18 | final TextStyle? textStyle; 19 | 20 | /// Style the text of the selected chips. 21 | final TextStyle? selectedTextStyle; 22 | 23 | /// The icon displayed in front of text on selected chips. 24 | final Icon? icon; 25 | 26 | /// Replaces the deafult search icon when searchable is true. 27 | final Icon? searchIcon; 28 | 29 | /// Replaces the default close search icon when searchable is true. 30 | final Icon? closeSearchIcon; 31 | 32 | /// Set a ShapeBorder. Typically a RoundedRectangularBorder. 33 | final ShapeBorder? chipShape; 34 | 35 | /// Defines the header text. 36 | final Text? title; 37 | 38 | /// Enables horizontal scrolling. Default is true. 39 | final bool scroll; 40 | 41 | /// A function that sets the color of selected items based on their value. 42 | final Color Function(V)? colorator; 43 | 44 | /// Fires when a chip is tapped. A good time to store the selected values. 45 | final Function(List)? onTap; 46 | 47 | /// Enables search functionality. 48 | final bool? searchable; 49 | 50 | /// Set the search hint. 51 | final String? searchHint; 52 | 53 | /// Set the TextStyle of the search hint. 54 | final TextStyle? searchHintStyle; 55 | 56 | /// Set the TextStyle of the text that gets typed into the search bar. 57 | final TextStyle? searchTextStyle; 58 | 59 | /// Set the header color. 60 | final Color? headerColor; 61 | 62 | /// Build a custom widget that gets created dynamically for each item. 63 | final Widget Function(MultiSelectItem, FormFieldState>)? 64 | itemBuilder; 65 | 66 | /// Set the height of the selectable area. 67 | final double? height; 68 | 69 | /// Make use of the ScrollController to automatically scroll through the list. 70 | final Function(ScrollController)? scrollControl; 71 | 72 | /// Define a HorizontalScrollBar. 73 | final HorizontalScrollBar? scrollBar; 74 | 75 | /// Determines whether to show the header. 76 | final bool showHeader; 77 | 78 | /// Set the width of the chip. 79 | final double? chipWidth; 80 | 81 | final List initialValue; 82 | final AutovalidateMode autovalidateMode; 83 | final FormFieldValidator>? validator; 84 | final FormFieldSetter>? onSaved; 85 | final GlobalKey? key; 86 | 87 | MultiSelectChipField({ 88 | required this.items, 89 | this.decoration, 90 | this.chipColor, 91 | this.selectedChipColor, 92 | this.colorator, 93 | this.textStyle, 94 | this.selectedTextStyle, 95 | this.icon, 96 | this.searchIcon, 97 | this.closeSearchIcon, 98 | this.chipShape, 99 | this.onTap, 100 | this.title, 101 | this.scroll = true, 102 | this.searchable, 103 | this.searchHint, 104 | this.searchHintStyle, 105 | this.searchTextStyle, 106 | this.headerColor, 107 | this.key, 108 | this.onSaved, 109 | this.validator, 110 | this.autovalidateMode = AutovalidateMode.disabled, 111 | this.initialValue = const [], 112 | this.itemBuilder, 113 | this.height, 114 | this.scrollControl, 115 | this.scrollBar, 116 | this.showHeader = true, 117 | this.chipWidth, 118 | }) : super( 119 | key: key, 120 | onSaved: onSaved, 121 | validator: validator, 122 | autovalidateMode: autovalidateMode, 123 | initialValue: initialValue, 124 | builder: (FormFieldState> state) { 125 | _MultiSelectChipFieldView view = _MultiSelectChipFieldView( 126 | items: items, 127 | decoration: decoration, 128 | chipColor: chipColor, 129 | selectedChipColor: selectedChipColor, 130 | colorator: colorator, 131 | textStyle: textStyle, 132 | selectedTextStyle: selectedTextStyle, 133 | icon: icon, 134 | searchIcon: searchIcon, 135 | closeSearchIcon: closeSearchIcon, 136 | chipShape: chipShape, 137 | onTap: onTap, 138 | title: title, 139 | scroll: scroll, 140 | initialValue: initialValue, 141 | searchable: searchable, 142 | searchHint: searchHint, 143 | searchHintStyle: searchHintStyle, 144 | searchTextStyle: searchTextStyle, 145 | headerColor: headerColor, 146 | itemBuilder: itemBuilder, 147 | height: height, 148 | scrollControl: scrollControl, 149 | scrollBar: scrollBar, 150 | showHeader: showHeader, 151 | chipWidth: chipWidth, 152 | ); 153 | return _MultiSelectChipFieldView.withState( 154 | view as _MultiSelectChipFieldView, state); 155 | }); 156 | } 157 | 158 | // ignore: must_be_immutable 159 | class _MultiSelectChipFieldView extends StatefulWidget 160 | with MultiSelectActions { 161 | final BoxDecoration? decoration; 162 | final List> items; 163 | final List>? selectedItems; 164 | final Color? chipColor; 165 | final Color? selectedChipColor; 166 | final TextStyle? textStyle; 167 | final TextStyle? selectedTextStyle; 168 | final Icon? icon; 169 | final Icon? searchIcon; 170 | final Icon? closeSearchIcon; 171 | final ShapeBorder? chipShape; 172 | final Text? title; 173 | final bool scroll; 174 | final bool? searchable; 175 | final String? searchHint; 176 | final TextStyle? searchHintStyle; 177 | final TextStyle? searchTextStyle; 178 | final List initialValue; 179 | final Color? Function(V)? colorator; 180 | final Function(List)? onTap; 181 | final Color? headerColor; 182 | final Widget Function(MultiSelectItem, FormFieldState>)? 183 | itemBuilder; 184 | final double? height; 185 | FormFieldState>? state; 186 | final Function(ScrollController)? scrollControl; 187 | final HorizontalScrollBar? scrollBar; 188 | final bool showHeader; 189 | final double? chipWidth; 190 | 191 | _MultiSelectChipFieldView({ 192 | required this.items, 193 | this.selectedItems, 194 | this.decoration, 195 | this.chipColor, 196 | this.selectedChipColor, 197 | this.colorator, 198 | this.textStyle, 199 | this.selectedTextStyle, 200 | this.icon, 201 | this.chipShape, 202 | this.onTap, 203 | this.title, 204 | this.scroll = true, 205 | this.initialValue = const [], 206 | this.searchable, 207 | this.searchHint, 208 | this.searchIcon, 209 | this.closeSearchIcon, 210 | this.searchHintStyle, 211 | this.searchTextStyle, 212 | this.headerColor, 213 | this.itemBuilder, 214 | this.height, 215 | this.scrollControl, 216 | this.scrollBar, 217 | this.showHeader = true, 218 | this.chipWidth, 219 | }); 220 | 221 | /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectChipField. 222 | _MultiSelectChipFieldView.withState( 223 | _MultiSelectChipFieldView field, FormFieldState> state) 224 | : items = field.items, 225 | title = field.title, 226 | decoration = field.decoration, 227 | initialValue = field.initialValue, 228 | selectedChipColor = field.selectedChipColor, 229 | chipShape = field.chipShape, 230 | colorator = field.colorator, 231 | chipColor = field.chipColor, 232 | icon = field.icon, 233 | closeSearchIcon = field.closeSearchIcon, 234 | selectedItems = field.selectedItems, 235 | textStyle = field.textStyle, 236 | scroll = field.scroll, 237 | selectedTextStyle = field.selectedTextStyle, 238 | onTap = field.onTap, 239 | searchable = field.searchable, 240 | searchHint = field.searchHint, 241 | searchIcon = field.searchIcon, 242 | searchTextStyle = field.searchTextStyle, 243 | searchHintStyle = field.searchHintStyle, 244 | headerColor = field.headerColor, 245 | itemBuilder = field.itemBuilder, 246 | height = field.height, 247 | scrollControl = field.scrollControl, 248 | scrollBar = field.scrollBar, 249 | showHeader = field.showHeader, 250 | chipWidth = field.chipWidth, 251 | state = state; 252 | 253 | @override 254 | __MultiSelectChipFieldViewState createState() => 255 | __MultiSelectChipFieldViewState(items); 256 | } 257 | 258 | class __MultiSelectChipFieldViewState 259 | extends State<_MultiSelectChipFieldView> { 260 | List _selectedValues = []; 261 | bool _showSearch = false; 262 | List _items; 263 | ScrollController _scrollController = ScrollController(); 264 | 265 | __MultiSelectChipFieldViewState(this._items); 266 | 267 | void initState() { 268 | super.initState(); 269 | _selectedValues.addAll(widget.initialValue); 270 | 271 | if (widget.scrollControl != null && widget.scroll) 272 | WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToPosition()); 273 | } 274 | 275 | @override 276 | void didUpdateWidget(_MultiSelectChipFieldView oldWidget) { 277 | super.didUpdateWidget(oldWidget); 278 | 279 | if (oldWidget.initialValue != widget.initialValue) { 280 | _selectedValues = []; 281 | _selectedValues.addAll(widget.initialValue); 282 | 283 | WidgetsBinding.instance.addPostFrameCallback((_) { 284 | widget.state!.didChange(_selectedValues); 285 | }); 286 | } 287 | if (oldWidget.items != widget.items) { 288 | _items = [...widget.items]; 289 | } 290 | } 291 | 292 | _scrollToPosition() { 293 | widget.scrollControl!(_scrollController); 294 | } 295 | 296 | @override 297 | Widget build(BuildContext context) { 298 | return Column( 299 | children: [ 300 | Container( 301 | decoration: widget.decoration ?? 302 | BoxDecoration( 303 | border: 304 | Border.all(width: 1, color: Theme.of(context).primaryColor), 305 | ), 306 | child: Column( 307 | children: [ 308 | widget.showHeader 309 | ? Container( 310 | color: 311 | widget.headerColor ?? Theme.of(context).primaryColor, 312 | child: Row( 313 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 314 | children: [ 315 | _showSearch 316 | ? Expanded( 317 | child: Container( 318 | padding: EdgeInsets.only(left: 10), 319 | child: TextField( 320 | style: widget.searchTextStyle, 321 | decoration: InputDecoration( 322 | hintStyle: widget.searchHintStyle, 323 | hintText: widget.searchHint ?? "Search", 324 | focusedBorder: UnderlineInputBorder( 325 | borderSide: BorderSide( 326 | color: widget.selectedChipColor ?? 327 | Theme.of(context).primaryColor, 328 | ), 329 | ), 330 | ), 331 | onChanged: (val) { 332 | setState(() { 333 | _items = widget.updateSearchQuery( 334 | val, widget.items); 335 | }); 336 | }, 337 | ), 338 | ), 339 | ) 340 | : Padding( 341 | padding: const EdgeInsets.only(left: 8.0), 342 | child: widget.title != null 343 | ? Text( 344 | widget.title!.data!, 345 | style: TextStyle( 346 | color: widget.title!.style != null 347 | ? widget.title!.style!.color 348 | : null, 349 | fontSize: 350 | widget.title!.style != null 351 | ? widget.title!.style! 352 | .fontSize ?? 353 | 18 354 | : 18), 355 | ) 356 | : Text( 357 | "Select", 358 | style: TextStyle(fontSize: 18), 359 | ), 360 | ), 361 | widget.searchable != null && widget.searchable! 362 | ? IconButton( 363 | icon: _showSearch 364 | ? widget.closeSearchIcon ?? 365 | Icon( 366 | Icons.close, 367 | size: 22, 368 | ) 369 | : widget.searchIcon ?? 370 | Icon( 371 | Icons.search, 372 | size: 22, 373 | ), 374 | onPressed: () { 375 | setState(() { 376 | _showSearch = !_showSearch; 377 | if (!_showSearch) _items = widget.items; 378 | }); 379 | }, 380 | ) 381 | : Padding( 382 | padding: EdgeInsets.all(18), 383 | ), 384 | ], 385 | ), 386 | ) 387 | : Container(), 388 | widget.scroll 389 | ? Container( 390 | padding: widget.itemBuilder == null 391 | ? EdgeInsets.symmetric(horizontal: 5) 392 | : null, 393 | width: MediaQuery.of(context).size.width, 394 | height: widget.height ?? 395 | MediaQuery.of(context).size.height * 0.08, 396 | child: widget.scrollBar != null 397 | ? Scrollbar( 398 | thumbVisibility: widget.scrollBar!.isAlwaysShown, 399 | controller: _scrollController, 400 | child: ListView.builder( 401 | controller: _scrollController, 402 | scrollDirection: Axis.horizontal, 403 | itemCount: _items.length, 404 | itemBuilder: (ctx, index) { 405 | return widget.itemBuilder != null 406 | ? widget.itemBuilder!( 407 | _items[index] as MultiSelectItem, 408 | widget.state!) 409 | : _buildItem( 410 | _items[index] as MultiSelectItem); 411 | }, 412 | ), 413 | ) 414 | : ListView.builder( 415 | controller: _scrollController, 416 | scrollDirection: Axis.horizontal, 417 | itemCount: _items.length, 418 | itemBuilder: (ctx, index) { 419 | return widget.itemBuilder != null 420 | ? widget.itemBuilder!( 421 | _items[index] as MultiSelectItem, 422 | widget.state!) 423 | : _buildItem( 424 | _items[index] as MultiSelectItem); 425 | }, 426 | ), 427 | ) 428 | : Container( 429 | height: widget.height, 430 | alignment: Alignment.centerLeft, 431 | padding: EdgeInsets.symmetric(horizontal: 10), 432 | child: Wrap( 433 | children: widget.itemBuilder != null 434 | ? _items 435 | .map((item) => widget.itemBuilder!( 436 | item as MultiSelectItem, widget.state!)) 437 | .toList() 438 | : _items 439 | .map((item) => 440 | _buildItem(item as MultiSelectItem)) 441 | .toList(), 442 | ), 443 | ), 444 | ], 445 | ), 446 | ), 447 | widget.state != null && widget.state!.hasError 448 | ? Row( 449 | children: [ 450 | Padding( 451 | padding: const EdgeInsets.fromLTRB(4, 4, 0, 0), 452 | child: Text( 453 | widget.state!.errorText!, 454 | style: TextStyle( 455 | color: Colors.red[800], 456 | fontSize: 12.5, 457 | ), 458 | ), 459 | ), 460 | ], 461 | ) 462 | : Container(), 463 | ], 464 | ); 465 | } 466 | 467 | Widget _buildItem(MultiSelectItem item) { 468 | return Container( 469 | margin: EdgeInsets.all(0), 470 | padding: const EdgeInsets.all(2.0), 471 | child: ChoiceChip( 472 | shape: widget.chipShape as OutlinedBorder? ?? 473 | RoundedRectangleBorder( 474 | side: BorderSide( 475 | color: widget.colorator != null && 476 | widget.colorator!(item.value) != null && 477 | _selectedValues.contains(item.value) 478 | ? widget.colorator!(item.value)! 479 | : widget.selectedChipColor ?? 480 | Theme.of(context).primaryColor), 481 | borderRadius: BorderRadius.vertical( 482 | top: Radius.circular(15.0), 483 | bottom: Radius.circular(15.0), 484 | ), 485 | ), 486 | avatar: _selectedValues.contains(item.value) 487 | ? widget.icon != null 488 | ? Icon( 489 | widget.icon!.icon, 490 | color: widget.colorator != null && 491 | widget.colorator!(item.value) != null 492 | ? widget.colorator!(item.value)!.withOpacity(1) 493 | : widget.icon!.color ?? 494 | widget.selectedChipColor ?? 495 | Theme.of(context).primaryColor, 496 | ) 497 | : null 498 | : null, 499 | label: Container( 500 | width: widget.chipWidth, 501 | child: Text( 502 | item.label, 503 | overflow: TextOverflow.ellipsis, 504 | style: _selectedValues.contains(item.value) 505 | ? TextStyle( 506 | color: widget.colorator != null && 507 | widget.colorator!(item.value) != null 508 | ? widget.colorator!(item.value)!.withOpacity(1) 509 | : widget.selectedTextStyle != null 510 | ? widget.selectedTextStyle!.color 511 | : null) 512 | : TextStyle( 513 | color: widget.textStyle != null 514 | ? widget.textStyle!.color ?? widget.chipColor 515 | : widget.chipColor, 516 | fontSize: widget.textStyle != null 517 | ? widget.textStyle!.fontSize 518 | : null, 519 | ), 520 | ), 521 | ), 522 | selected: _selectedValues.contains(item.value), 523 | backgroundColor: widget.chipColor ?? Colors.white70, 524 | selectedColor: 525 | widget.colorator != null && widget.colorator!(item.value) != null 526 | ? widget.colorator!(item.value) 527 | : widget.selectedChipColor != null 528 | ? widget.selectedChipColor 529 | : Theme.of(context).primaryColor.withOpacity(0.33), 530 | onSelected: (_) { 531 | if (_) { 532 | _selectedValues.add(item.value); 533 | if (widget.state != null) { 534 | widget.state!.didChange(_selectedValues); 535 | } 536 | } else { 537 | _selectedValues.remove(item.value); 538 | if (widget.state != null) { 539 | widget.state!.didChange(_selectedValues); 540 | } 541 | } 542 | if (widget.onTap != null) widget.onTap!(_selectedValues); 543 | }, 544 | ), 545 | ); 546 | } 547 | } 548 | --------------------------------------------------------------------------------