FlutterFormPro
English
| ภาษาไทย
Professional, extensible form validation and widgets for Flutter with built-in i18n (default Thai) and many ready-to-use validators. This package supports both standalone validators and a small reactive widget layer.

Docs: Accessibility (a11y) • Design Notes (Large/Conditional/Async)
Features
- Rich validator set (string, number, list, date/time, network, security, Thai-specific, credit card, etc.)
- Localized error messages via JSON assets (th default; en/zh/ja/fr included)
- High performance: cached i18n maps and precompiled RegExp
- Reactive widgets: Material/Cupertino text fields, Autocomplete, number and date picker fields, submit and keyboard-submit helpers
Why FlutterFormPro?
- Performance-first: reactive ChangeNotifier form core, minimal rebuilds, precompiled RegExp, and cached i18n maps.
- Thai-first i18n: ships with Thai defaults and fallbacks so error messages work even before assets are loaded.
- Flexible by design: use Validators standalone or opt into the FormPro widget set; customize via FormProField when you need full control.
Install
Add the package to your pubspec.yaml
and declare the i18n assets (already configured in this repo):
dependencies:
flutter_form_pro: ^0.0.7
Notes:
- When used as a dependency, the package loads JSON via
packages/flutter_form_pro/assets/i18n/...
automatically. - Default locale is Thai (
th
). If you don’t set a locale, Thai will be used.
Getting started
Assets are already configured under assets/i18n/
in pubspec.
Initialize i18n early (optional, default is th
):
import 'package:flutter_form_pro/flutter_form_pro.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FormProI18n.setLocaleAndLoad('en'); // or keep default 'th'
runApp(const MyApp());
}
Usage (Validators only)
TextFormField(
validator: Validators.multi([
Validators.required(),
Validators.email(),
Validators.minLength(6),
]),
)
More examples:
Validators.isUUIDv4();
Validators.isUrl();
Validators.passwordNumberTextSpecial();
Validators.isThaiPhone();
Validators.isFutureDate();
// International calling codes
Validators.isCallingCode(); // requires leading '+' (e.g., '+66')
Validators.isCallingCode(null, false); // allow '66' (no '+')
Validators.isCallingCode(null, true, { '+999': 'TestLand' }); // custom map
final name = Validators.callingCodeNameOf('+447911123456'); // 'United Kingdom'
// Thai ID laser code (AAA-XXXXXXX-XX) or hyphenless
Validators.isThaiLaserId(); // accepts 'JT0-1234567-12' and 'JT0123456712'
Validators.isThaiLaserId('Invalid', false); // require dashes only
Optional fields made easy
- Optional email field (empty is allowed; if provided, must be valid and <= 64 chars):
TextFormField(
validator: Validators.optional([
Validators.email(),
Validators.maxLength(64),
]),
)
- Wrap single validators with whenNotEmpty for optional fields:
TextFormField(
validator: Validators.multi([
Validators.whenNotEmpty(Validators.email()),
Validators.whenNotEmpty(Validators.maxLength(64)),
]),
)
Example app
See the runnable example in example/
.
Quick peek:
import 'package:flutter/material.dart';
import 'package:flutter_form_pro/flutter_form_pro.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await FormProI18n.setLocaleAndLoad('th');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'FlutterFormPro Example',
home: const DemoPage(),
);
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
final _formKey = GlobalKey<FormState>();
String _email = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('FlutterFormPro Example')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
onChanged: (v) => _email = v,
validator: Validators.multi([
Validators.required(),
Validators.email(),
Validators.maxLength(64),
]),
),
const SizedBox(height: 16),
Row(
children: [
ElevatedButton(
onPressed: () {
final ok = _formKey.currentState!.validate();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(ok ? 'Valid' : 'Invalid')),
);
},
child: const Text('Validate'),
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: () async {
await FormProI18n.setLocaleAndLoad('en');
setState(() {});
},
child: const Text('EN'),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: () async {
await FormProI18n.setLocaleAndLoad('th');
setState(() {});
},
child: const Text('TH'),
),
],
),
const SizedBox(height: 8),
Text('Current email: $_email'),
],
),
),
),
);
}
}
Locale switching
await FormProI18n.setLocaleAndLoad('th');
// or preload for fast switching
await FormProI18n.preload(['th','en','ja']);
Core concepts
- Validator:
typedef Validator = String? Function(dynamic value)
- Return
null
when valid; return a localized message when invalid.
- Return
Validators
class: static factory methods for common checks.- Compose multiple validators with
Validators.multi([...])
(returns the first error).
Common validator recipes
- Text
Validators.required()
Validators.minLength(6)
,Validators.maxLength(32)
Validators.isAlpha()
,Validators.isAlphanumeric()
,Validators.isAscii()
Validators.onlyThaiText()
,Validators.onlyEnglishText()
Validators.isLowerCase()
,Validators.isUpperCase()
- Password policy:
passwordNumber
,passwordNumberText
,passwordNumberOrText
,passwordNumberTextSpecial
,passwordStrong
,passwordWeak
- Number
Validators.isInt()
,Validators.isDouble()
Validators.isPositive()
,Validators.isNegative()
Validators.isEven()
,Validators.isOdd()
Validators.isMultipleOf(n)
- List / Enum
Validators.isInList(['a','b'])
,Validators.isNotInList([...])
Validators.isEnumValue(MyEnum.values)
(or any list of values)
- Date / Time (format:
YYYY-MM-DD
, e.g.,2025-01-31
)Validators.date()
,Validators.datetime()
(YYYY-MM-DD HH:mm:ss)Validators.isFutureDate()
,Validators.isPastDate()
- Network / Format
Validators.email()
,Validators.isEmailDomain('example.com')
Validators.isUrl()
,Validators.isJson()
,Validators.isHexColor()
- Identity / Security
Validators.username()
,Validators.phone()
Validators.otp()
,Validators.pin()
Validators.creditCard()
,Validators.isVisaCard()
,Validators.isMasterCard()
- Thai-specific
Validators.isThaiPhone()
,Validators.isThaiZipCode()
Validators.nationalId()
- Misc
Validators.uuid
(v4),Validators.isIPv4()
,Validators.isIPv6()
Validators.isCurrency()
,Validators.isBaht()
,Validators.isDollar()
Validators.isFileExtension(['.png','.jpg'])
Validators.noWhitespace()
,Validators.noRepeatChar()
,Validators.noSequentialChar()
Validators.isPalindrome()
,Validators.isPrime()
Custom validators
- Quick predicate:
TextFormField(
validator: Validators.custom((v) => (v is String) && v.startsWith('F')), // null when true
)
- Invert a pattern (
notMatch
):
final noDigits = Validators.notMatch(RegExp(r"\d"), 'Digits are not allowed');
- Compose:
final v = Validators.multi([
Validators.required(),
Validators.email(),
Validators.maxLength(64),
]);
Localization (i18n)
- Default locale:
th
- Switch locale at runtime:
await FormProI18n.setLocaleAndLoad('en');
// Optional: FormProI18n.preload(['th','en','ja']);
- Override a message at runtime:
FormProI18n.add('required', 'This field cannot be empty');
- Add your locale: create
assets/i18n/<code>.json
, then
await FormProI18n.setLocaleAndLoad('<code>');
The package falls back to Thai if the desired locale can’t be loaded. Core keys have built‑in Thai fallbacks to avoid empty strings even before assets load.
Clean Architecture note (Messages abstraction)
- Validators use a domain-level message abstraction
Messages.t(key)
instead of talking to i18n directly. - When you call
await FormProI18n.setLocaleAndLoad('xx')
, the package auto-bridgesMessages
toFormProI18n.t(...)
so everything works out of the box. - Special case: If your app has its own localization system, you can plug it in:
import 'package:flutter_form_pro/flutter_form_pro.dart';
void setupMyL10nBridge() {
Messages.setProvider((key, {fallback}) {
// Example: use your app localization; fall back to package defaults
final v = AppLocalizations.of(context)?.translate(key);
return v ?? fallback ?? key;
});
}
If you don’t set a provider, Messages
falls back to Thai defaults for common keys.
Custom messages and defaults
- Per‑field custom message (pass a message into each validator):
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: Validators.multi([
Validators.required('Please enter your email'),
Validators.email('Invalid email address'),
Validators.maxLength(64, 'Max 64 characters'),
]),
)
- Global override via i18n at runtime (affects all validators that use that key):
await FormProI18n.setLocaleAndLoad('en');
FormProI18n.add('required', 'This field cannot be empty');
FormProI18n.add('email', 'Please enter a valid email');
// Now Validators.required() / Validators.email() use your custom messages.
- Persist custom messages by editing your app’s
assets/i18n/<locale>.json
and shipping them with your app (recommended for teamwork/CI):
{
"required": "This field cannot be empty",
"email": "Please enter a valid email"
}
- Use flutter_form_pro defaults (no setup):
If you don’t load any locale, flutter_form_pro uses Thai (th
) by default and includes built‑in Thai fallbacks, so messages work out of the box.
void main() {
// No i18n initialization — defaults to 'th'
runApp(const MyApp());
}
// Later in UI
TextFormField(
validator: Validators.required(), // Shows Thai message by default
)
Performance tips
- i18n maps are cached per locale; use
preload([...])
if switching often. Validators
use precompiled RegExp—reuse validators when possible.- Short‑circuit empty values (e.g., skip heavy checks when
required
already fails).
Troubleshooting
- “Missing i18n”: ensure
assets/i18n/
exists and paths are declared underflutter: assets:
inpubspec.yaml
. - When using this package as a dependency, asset loads use
packages/flutter_form_pro/...
automatically (no extra config needed). - Still seeing English/keys? Call
await FormProI18n.ensureLoaded()
or set a locale during app init.
Testing
Run unit tests:
flutter test
The test suite includes edge cases for date/time boundaries and list/enum negatives.
Form widgets (reactive)
Build a reactive form with FormPro (ChangeNotifier-based) and use ready-to-go widgets.
import 'package:flutter_form_pro/flutter_form_pro.dart';
late final FormPro form;
@override
void initState() {
super.initState();
form = FormPro.builder()
.addField('email', FormFieldConfig(validators: [Validators.required(), Validators.email()]))
.addField('username', FormFieldConfig(validators: [Validators.required()]))
.addField('city', FormFieldConfig(validators: [Validators.required()]))
.build();
}
@override
Widget build(BuildContext context) {
return FormProWidget(
form: form,
onSubmit: (values) => debugPrint('submit: $values'),
child: const FormProKeyboardSubmit(
// Press Enter/NumpadEnter to submit
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12),
FormProTextField(
formFieldName: 'email',
decoration: InputDecoration(labelText: 'Email'),
),
SizedBox(height: 12),
// Controller-friendly version for advanced cursor control
FormProTextControllerField(
formFieldName: 'username',
decoration: InputDecoration(labelText: 'Username'),
),
],
),
),
);
}
Autocomplete and keyboard submit in one:
FormProAutocomplete<String>(
formFieldName: 'city',
optionsBuilder: (text) {
const cities = <String>{'Bangkok', 'Chiang Mai', 'Phuket', 'Khon Kaen'};
final q = text.text.toLowerCase();
return cities.where((c) => c.toLowerCase().contains(q));
},
displayStringForOption: (s) => s,
decoration: const InputDecoration(labelText: 'City'),
),
const SizedBox(height: 16),
const FormProSubmitButton(child: Text('Submit')),
Cupertino-style field:
const FormProCupertinoTextField(
formFieldName: 'email',
placeholder: 'Email',
)
Fully custom UI with builder (use your own controllers if needed):
final myController = TextEditingController();
...
FormProField<String>(
name: 'username',
builder: (ctx, value, error, onChanged) => TextField(
controller: myController..text = value ?? '',
decoration: InputDecoration(labelText: 'Custom Username', errorText: error),
onChanged: onChanged,
),
)
Additional widgets:
// Numbers only (with optional sign/decimal)
const FormProNumberField(
formFieldName: 'amount',
decoration: InputDecoration(labelText: 'Amount'),
allowDecimal: true,
signed: false,
)
// Date picker (stores `YYYY-MM-DD`)
const FormProDatePickerField(
formFieldName: 'birthday',
decoration: InputDecoration(labelText: 'Birthday'),
)
// Phone input (auto strips separators and '+66' -> local '0' by default)
const FormProPhoneField(
formFieldName: 'phone',
// Customize normalization per country if needed:
// stripDialCodes: ['+66', '+1'],
// replaceWithLeadingZero: true,
)
Pub points checklist (docs & quality)
- API docs: Public classes and constructors include concise dartdoc (FormPro, FormProWidget, FormProField, fields, validators, i18n).
- README: Contains badges, feature list, usage, examples, troubleshooting, and testing instructions.
- Example: A runnable example is included under
example/
. - License: MIT license provided.
- CI: Workflow runs
flutter test
on pushes/PRs.
If your pub score flags documentation coverage, consider adding more dartdoc to any newly added public members.
Libraries
- flutter_form_pro
- FlutterFormPro - แพ็คเกจจัดการฟอร์มอัจฉริยะ