A production-grade, type-safe Flutter form field package that provides a single public widget (SmartFormField) capable of rendering multiple input types through a sealed configuration hierarchy.
| Type | Config | Status |
|---|---|---|
| Text | SmartTextConfig |
✅ Complete |
| Password | SmartPasswordConfig |
✅ Complete |
| Phone | SmartPhoneConfig |
✅ Complete |
| OTP | SmartOtpConfig |
⏳ Stub |
| Date | SmartDateConfig |
⏳ Stub |
| Dropdown | SmartDropdownConfig |
⏳ Stub |
| File | SmartFileConfig |
⏳ Stub |
- 📱 Supported Platforms: Tested and officially supported on Android & iOS.
- 🎯 Single Public Widget:
SmartFormField(config: ...)for every input type. - 🔒 Type-Safe Sealed Configs:
SmartFieldConfigis sealed — compile-time safety, zero dead properties. - 🛡️ Built-in Validation: Required check,
minLength,maxLength, custom validator chain (SmartValidators.email). - 🔑 Password Strength & Custom Scoring: Animated 4-segment strength meter, custom scoring algorithms, custom meter UI, obscuring character selection (
•,*), custom eye icons. - 🔄 Confirm Password Matching: Real-time exact string match validation via
confirmPasswordController. - ✍️ Formatting & Capitalization:
autoCapitalizeWords, custominputFormatters, auto-trimming. - 🔌 External Controller: Optional
controllerproperty — you own disposal if provided, package handles it if omitted. - 🎨 Automatic Theming: Seamlessly inherits app
InputDecorationThemeand light/darkColorScheme. - ☎️ Phone Field with Real Validation: Country-code picker with flags,
libphonenumber-derived per-country format validation, and E.164-formatted output — no manual dial-code string manipulation needed. - ⏱ Debounced & Async Search:
SmartTextConfig.search()with race-condition-safe async execution. - 🔑 Programmatic Control: Validate, reset, read values via
GlobalKey<SmartBaseShellState>.
dependencies:
smart_multi_form_fields: ^1.1.0import 'package:smart_multi_form_fields/smart_multi_form_fields.dart';| Basic Text & Email Validation | Bio Multiline & Character Counter |
|---|---|
![]() |
![]() |
SmartFormField(
config: SmartTextConfig(
label: 'Full Name',
hint: 'John Doe',
isRequired: true,
autoCapitalizeWords: true,
prefixIcon: const Icon(Icons.person),
validators: [
(value) => value!.length < 3 ? 'Name must be at least 3 characters' : null,
],
),
)SmartFormField(
config: SmartTextConfig(
label: 'Email Address',
hint: 'user@example.com',
isRequired: true,
validators: [SmartValidators.email],
prefixIcon: const Icon(Icons.email),
),
)SmartFormField(
config: SmartTextConfig(
label: 'Bio',
hint: 'Write a short bio...',
minLength: 10,
maxLength: 200,
maxLines: 4,
minLines: 2,
prefixIcon: const Icon(Icons.info),
),
)SmartFormField(
config: SmartTextConfig(
label: 'Address',
hint: 'Enter your address',
showClearButton: true, // Auto-shows (×) clear icon when non-empty
prefixIcon: const Icon(Icons.location_on),
),
)SmartFormField(
config: SmartTextConfig.search(
label: 'Search Products',
hint: 'Type product name...',
debounce: const Duration(milliseconds: 400),
onSearchAsync: (query, isCurrent) async {
final results = await myApi.searchProducts(query);
if (isCurrent()) { // Ensures older, slower API responses don't overwrite newer results
setState(() => _searchResults = results);
}
},
),
)final _nameController = TextEditingController();
final _nextFocus = FocusNode();
SmartFormField(
config: SmartTextConfig(
label: 'First Name',
controller: _nameController,
nextFocusNode: _nextFocus,
textInputAction: TextInputAction.next,
),
)
// Read or pre-fill value anytime:
_nameController.text = 'Pre-filled';final _formKey = GlobalKey<SmartBaseShellState>();
SmartFormField(
key: _formKey,
config: SmartTextConfig(label: 'Username', isRequired: true),
)
// On submit button click:
void onSubmit() {
final error = _formKey.currentState?.validate();
if (error == null) {
final value = _formKey.currentState?.value;
print('Valid value: $value');
}
}| Strength Meter & Custom Policy | Custom Validators & Live Values Card |
|---|---|
![]() |
![]() |
SmartFormField(
config: SmartPasswordConfig(
label: 'Password',
hint: 'Enter your password',
isRequired: true,
minPasswordLength: 8,
showStrengthMeter: true, // Animated 4-segment strength bar
),
)SmartFormField(
config: SmartPasswordConfig(
label: 'Strong Password',
isRequired: true,
minPasswordLength: 8,
requireUppercase: true, // Must include A-Z
requireLowercase: true, // Must include a-z
requireDigit: true, // Must include 0-9
requireSpecialChar: true, // Must include !@#$%^&*
),
)final _passwordController = TextEditingController();
// Original password field
SmartFormField(
config: SmartPasswordConfig(
label: 'Password',
controller: _passwordController,
isRequired: true,
),
)
// Confirm password field — compares value against original field
SmartFormField(
config: SmartPasswordConfig(
label: 'Confirm Password',
confirmPasswordController: _passwordController,
confirmMismatchMessage: 'Passwords do not match',
showStrengthMeter: false,
isRequired: true,
),
)SmartFormField(
config: SmartPasswordConfig(
label: 'Custom Password',
obscuringCharacter: '*', // Mask symbol (* instead of default •)
toggleIconBuilder: (isObscured) => Icon(
isObscured ? Icons.lock_outline : Icons.lock_open_outlined,
color: Colors.blue,
size: 20,
),
),
)SmartFormField(
config: SmartPasswordConfig(
label: 'Custom Policy Password',
// Custom scoring algorithm
customStrengthScorer: (password, minLength) {
if (password.length < minLength) return PasswordStrength.weak;
if (password.contains('123456')) return PasswordStrength.weak;
return PasswordStrengthScorer.score(password, minLength: minLength);
},
// Custom strength meter UI replacement
strengthMeterBuilder: (context, strength) {
return Text('Strength: ${strength.label}', style: TextStyle(color: strength.color));
},
),
)| Phone Field Implementation | Validation & Selection |
|---|---|
![]() |
![]() |
SmartFormField(
config: SmartPhoneConfig(
label: 'Phone Number',
defaultCountryCode: 'IN', // required — pick your app's primary market
isRequired: true,
helperText: 'We\'ll text you a verification code',
),
)SmartFormField(
config: SmartPhoneConfig(
label: 'Phone Number',
defaultCountryCode: 'GB',
invalidNumberMessage: 'That doesn\'t look like a valid number',
onCountryChanged: (isoCode, dialCode) {
print('User switched to $isoCode ($dialCode)');
},
),
)final _phoneKey = GlobalKey<SmartBaseShellState>();
SmartFormField(
key: _phoneKey,
config: SmartPhoneConfig(label: 'Phone', defaultCountryCode: 'US'),
)
// Always returns the full international format, e.g. "+14155551234":
final phone = _phoneKey.currentState?.value;Important: defaultCountryCode is required by design — this package does not attempt to auto-detect a user's country from device locale, since a phone's display-language setting is not a reliable proxy for actual location. Set it to whichever country your app primarily targets; users elsewhere can switch via the flag picker at any time.
| Property | Type | Description |
|---|---|---|
label |
String? |
Field label header shown above input |
hint |
String? |
Placeholder text inside input |
helperText |
String? |
Supporting text shown below field |
isRequired |
bool |
Adds * asterisk and runs empty validation |
validators |
List<SmartValidator> |
Custom validation functions chain |
minLength |
int? |
Minimum character length constraint |
maxLength |
int? |
Maximum character length constraint (renders counter) |
maxLines |
int? |
Maximum lines for multiline input |
minLines |
int? |
Minimum lines for multiline input |
autoCapitalizeWords |
bool |
Automatically capitalizes first letter of every word |
showClearButton |
bool |
Renders (×) clear button when text is non-empty |
prefixIcon |
Widget? |
Icon displayed at the start of input |
suffixIcon |
Widget? |
Icon displayed at the end of input |
onSuffixIconTap |
VoidCallback? |
Tap handler for suffixIcon |
readOnly |
bool |
Focusable and selectable but prevents editing |
enabled |
bool |
Disables field interactions and dims colors |
controller |
TextEditingController? |
External controller override |
focusNode |
FocusNode? |
External focus node override |
nextFocusNode |
FocusNode? |
Focus node to request on keyboard submit |
debounce |
Duration |
Delay for debounced callbacks (default: 300ms) |
onDebouncedChanged |
ValueChanged<String>? |
Sync callback invoked after debounce delay |
onSearchAsync |
Future<void> Function(query, isCurrent)? |
Async search callback with stale response checker |
showSearchLoadingIndicator |
bool |
Auto-swaps suffix icon to spinner during async search |
| Property | Type | Description |
|---|---|---|
label |
String? |
Field label header shown above input |
hint |
String? |
Placeholder text inside input |
helperText |
String? |
Supporting text shown below field |
isRequired |
bool |
Adds * asterisk and runs empty validation |
obscuringCharacter |
String |
Mask symbol used when obscured (default: '•') |
showToggleIcon |
bool |
Renders visibility eye icon toggle (default: true) |
toggleIconBuilder |
Widget Function(bool isObscured)? |
Builder for custom visibility toggle icon |
showStrengthMeter |
bool |
Renders animated 4-segment password strength bar (default: true) |
strengthMeterBuilder |
Widget Function(BuildContext, PasswordStrength)? |
Builder for replacing default strength meter UI |
customStrengthScorer |
PasswordStrength Function(String, int)? |
Custom algorithm function for password strength scoring |
minPasswordLength |
int |
Minimum password length requirement & strength threshold baseline (default: 8) |
requireUppercase |
bool |
Requires at least one uppercase letter [A-Z] |
requireLowercase |
bool |
Requires at least one lowercase letter [a-z] |
requireDigit |
bool |
Requires at least one numeric digit [0-9] |
requireSpecialChar |
bool |
Requires at least one special character [!@#$%^&*...] |
confirmPasswordController |
TextEditingController? |
Controller of original password field for match validation |
confirmMismatchMessage |
String? |
Custom error message when confirm password does not match |
| Property | Type | Description |
|---|---|---|
label |
String? |
Field label header shown above input |
hint |
String? |
Placeholder text inside input |
helperText |
String? |
Supporting text shown below field |
isRequired |
bool |
Adds * asterisk and runs empty validation |
defaultCountryCode |
String |
Required. ISO 3166-1 alpha-2 starting country (e.g. 'IN', 'US', 'GB') |
onCountryChanged |
void Function(String isoCode, String dialCode)? |
Fires when the user picks a different country |
invalidNumberMessage |
String? |
Custom error message for format validation failure |
showDropdownIcon |
bool |
Show/hide the chevron next to the flag (flag stays tappable either way) |
flagsButtonPadding |
EdgeInsetsGeometry? |
Padding around the flag/dial-code button |
flagsButtonMargin |
EdgeInsets? |
Margin around the flag/dial-code button |
validateMode |
AutovalidateMode |
Controls live re-validation behavior as the user types |
controller |
TextEditingController? |
External controller override (holds the national number) |
focusNode / nextFocusNode |
FocusNode? |
External focus control / focus traversal target |
validators |
List<SmartValidator> |
Custom validators — receive the raw national number, no dial code |
Form building in Flutter shouldn't require copy-pasting hundreds of lines of TextFormField boilerplate or mixing separate third-party packages for every input type.
smart_multi_form_fields unifies all your form fields into a single, production-grade widget with compile-time type safety, theme adaptability, built-in validation rules, animated strength feedback, and zero maintenance bloat.
If this package saves you development time or makes your Flutter codebase cleaner, please ⭐ star the repository on GitHub to support the project!
We welcome feature requests, bug reports, and pull requests! To ensure issues are resolved as quickly as possible, please follow these guidelines when opening a GitHub issue.
- Check existing GitHub Issues to verify your bug or feature request hasn't already been reported.
- Click New Issue on the repository's Issues tab.
- Provide the required details listed below.
- 📌 Environment Info: Include output of
flutter doctor -v(Flutter SDK and Dart versions). - 📦 Package Version: State the package version used (e.g.
smart_multi_form_fields: ^1.1.0). - 🧩 Config Type: Specify which configuration was used (e.g.
SmartTextConfigorSmartPasswordConfig). - 📝 Minimal Reproducible Code: Provide a complete, self-contained Flutter snippet reproducing the behavior.
- 🎯 Expected vs Actual Behavior: Clearly describe what should happen vs what actually occurred.
- 📷 Screenshots / Logs: Attach error stack traces or visual screenshots if applicable.
MIT License.





