Skip to content

Eng-MahmoudBasuony/x_validators

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

68 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Stand With Palestine

β˜• X Validators

Pub Version License

The simplest way to validate Flutter form fields β€” a small, composable set of rules that plug straight into any TextFormField.validator.

  • βœ… Zero dependencies β€” pure Dart, nothing to ship but your code
  • 🧩 Composable β€” stack rules; the first failure wins
  • πŸ”Œ Extensible β€” write your own rule by implementing one method
  • 🌍 Localizable β€” translate every error message per rule type
  • πŸ§ͺ Well tested β€” every rule is covered by unit tests

Table of Contents

πŸš€ Installation

Add the dependency to your pubspec.yaml:

dependencies:
  x_validators: ^2.0.0

Then run flutter pub get (or dart pub get).

import 'package:x_validators/x_validators.dart';

⚑ Quick start

xValidator takes a list of rules and returns a String? Function(String?) β€” exactly the signature Flutter's form fields expect:

TextFormField(
  decoration: const InputDecoration(labelText: 'Email'),
  validator: xValidator([
    const IsRequired(),
    const IsEmail(),
  ]),
);

The returned function yields null when the value is valid, or the error message of the first failing rule otherwise.

🧠 How it works

Every rule is a small class that extends TextXValidationRule and answers one question β€” bool isValid(String input). xValidator runs the rules in order and stops at the first one that fails:

xValidator([ rule₁, ruleβ‚‚, rule₃ ])  ──▢  String? Function(String? value)
                                              β”‚
        value ─▢ rule₁.isValid? ─ yes ─▢ ruleβ‚‚.isValid? ─ yes ─▢ rule₃ ... ─▢ null  (valid)
                      β”‚ no                     β”‚ no
                      β–Ό                        β–Ό
                  error message            error message            (first failure wins)

Because order matters, put broad rules first (e.g. IsRequired) and specific rules after (e.g. IsEmail).

πŸ“š API reference

Most rules accept an optional positional error message as their last argument (e.g. IsRequired('This field is required')); a few (Match, ContainsAny) take it as a named error parameter alongside their other named options. When omitted, the rule's default message is used (see Localizing error messages).

Text

Rule Description
IsRequired([error]) Fails when the trimmed input is empty.
IsEmpty([error]) Passes only when the trimmed input is empty.
Contains(value, [error]) Input contains value (compared after trimming).
NotContains(value, [error]) Input does not contain value.
StartsWith(prefix, [error]) Input starts with prefix.
EndsWith(suffix, [error]) Input ends with suffix.
Match(value, {caseSensitive = true}, [error]) Input equals value; case-insensitive when caseSensitive is false.
MinLength(min, [error]) Trimmed length is >= min.
MaxLength(max, [error]) Trimmed length is <= max.

Numbers

Rule Description
IsNumber([error]) Input is an integer (use IsDecimal for fractional values).
IsDecimal([error]) Input is a decimal number (integers are accepted too).
IsArabicNum([error]) Positive integer written in Latin digits 0-9 (no leading zero).
IsHindiNum([error]) Number written in Arabic-Indic digits Ω -Ω©.
MinValue(min, [error]) Parsed numeric value is >= min.
MaxValue(max, [error]) Parsed numeric value is <= max.

URLs

Rule Description
IsUrl([error]) Input is a valid http/https URL.
IsSecureUrl([error]) Input is a URL using the https:// scheme.
IsFacebookUrl([error]) Input is a valid Facebook URL.
IsInstagramUrl([error]) Input is a valid Instagram URL.
IsYoutubeUrl([error]) Input is a valid YouTube URL.

Phone

Rule Description
IsEgyptianPhone([error]) Input is a valid Egyptian phone number.
ISKsaPhone([error]) Input is a valid Saudi Arabian phone number.

IT

Rule Description
IsEmail([error]) Input is a valid email address.
IsBool([error]) Input is a valid boolean value.
IsIpAddress([error]) Input is a valid IP address.
IsPort([error]) Input is a valid port number.
RegExpRule(regExp, [error]) Input matches the provided RegExp.

Lists

Rule Description
IsIn(values, [error]) Input is one of the values in the provided list.
IsNotIn(values, [error]) Input is not in the provided list.
ContainsAny(values, {caseSensitive = false, error}) Input contains at least one item from the list (case-insensitive by default).
NotContainsAny(values, [error]) Input contains none of the items in the list.

Dates

Rule Description
IsDate([error]) Input is a parseable date string.
IsDateMillis([error]) Input is an integer of milliseconds since epoch.
IsDateAfter(date, [error]) Input is a date later than date.

Languages

Rule Description
IsArabicChars([error]) Input consists of Arabic letters, whitespace and Arabic-Indic digits Ω -Ω©.
IsEnglishChars([error]) Only English letters A–Z (no spaces or digits).
IsNumbersOnly([error]) Input is all digits (one or more).
IsLtrLanguage([error]) Input is a left-to-right language code.
IsRTLLanguage([error]) Input is a right-to-left language code.

Colors

Rule Description
IsHexColor([error]) Input is a valid 3/6/8-digit hex color.

Magic

Rule Description
IsOptional() When present, an empty field skips all other rules and passes.

Examples for every rule

Every rule is shown below with a passing and a failing input. The examples use rule.isValid(value) so you can see the boolean directly; inside a form you'd normally pass the rule to xValidator([...]) and get the error message instead (see Composing rules). Each rule also has a matching top-level function (isEmail, isHexColor, …) for one-off checks β€” see Standalone helper functions.

Text

const IsRequired().isValid('Jane');           // β†’ true
const IsRequired().isValid('   ');            // β†’ false  (trimmed empty)

const IsEmpty().isValid('   ');               // β†’ true
const IsEmpty().isValid('x');                 // β†’ false

const Contains('@').isValid('a@b.com');       // β†’ true
const Contains('@').isValid('abc');           // β†’ false

const NotContains(' ').isValid('no_spaces');  // β†’ true
const NotContains(' ').isValid('has space');  // β†’ false

const StartsWith('+20').isValid('+20100');    // β†’ true
const StartsWith('+20').isValid('0100');      // β†’ false

const EndsWith('.com').isValid('site.com');   // β†’ true
const EndsWith('.com').isValid('site.org');   // β†’ false

const Match('Yes').isValid('Yes');            // β†’ true   (case-sensitive by default)
const Match('Yes').isValid('yes');            // β†’ false
const Match('Yes', caseSensitive: false).isValid('yes'); // β†’ true

const MinLength(3).isValid('abc');            // β†’ true
const MinLength(3).isValid('ab');             // β†’ false

const MaxLength(5).isValid('abcde');          // β†’ true
const MaxLength(5).isValid('abcdef');         // β†’ false

Numbers

const IsNumber().isValid('42');               // β†’ true
const IsNumber().isValid('-7');               // β†’ true
const IsNumber().isValid('3.14');             // β†’ false  (use IsDecimal)
const IsNumber().isValid('0x1A');             // β†’ false  (no hex)

const IsDecimal().isValid('3.14');            // β†’ true
const IsDecimal().isValid('42');              // β†’ true   (integers too)
const IsDecimal().isValid('abc');             // β†’ false

const IsArabicNum().isValid('123');           // β†’ true   (Latin digits, positive)
const IsArabicNum().isValid('012');           // β†’ false  (leading zero)
const IsArabicNum().isValid('0');             // β†’ false

const IsHindiNum().isValid('Ω‘Ω’Ω£');             // β†’ true   (Arabic-Indic digits)
const IsHindiNum().isValid('Ω Ω‘Ω’');             // β†’ false  (leading Ω )
const IsHindiNum().isValid('123');            // β†’ false  (Latin digits)

const MinValue(18).isValid('18');             // β†’ true
const MinValue(18).isValid('17');             // β†’ false

const MaxValue(100).isValid('100');           // β†’ true
const MaxValue(100).isValid('101');           // β†’ false

URLs

const IsUrl().isValid('https://my-site.co.uk');         // β†’ true
const IsUrl().isValid('https://a.b.example.com/p?x=1'); // β†’ true
const IsUrl().isValid('example.com');                   // β†’ false  (no scheme)

const IsSecureUrl().isValid('https://x.com');           // β†’ true
const IsSecureUrl().isValid('http://x.com');            // β†’ false

const IsFacebookUrl().isValid('https://facebook.com/me');       // β†’ true
const IsFacebookUrl().isValid('https://facebook.com.evil.com'); // β†’ false

const IsInstagramUrl().isValid('https://instagram.com/me');     // β†’ true
const IsInstagramUrl().isValid('https://evil.com');             // β†’ false

const IsYoutubeUrl().isValid('https://youtube.com/watch?v=x');  // β†’ true
const IsYoutubeUrl().isValid('https://vimeo.com/1');            // β†’ false

Phone

const IsEgyptianPhone().isValid('01012345678'); // β†’ true   (010/011/012/015 + 8 digits)
const IsEgyptianPhone().isValid('01312345678'); // β†’ false  (013 is not a valid prefix)

const ISKsaPhone().isValid('0512345678');       // β†’ true
const ISKsaPhone().isValid('+966512345678');    // β†’ true
const ISKsaPhone().isValid('0612345678');       // β†’ false

IT

const IsEmail().isValid('jane.doe@example.com'); // β†’ true
const IsEmail().isValid('plainaddress');         // β†’ false

const IsBool().isValid('true');                  // β†’ true
const IsBool().isValid(' FALSE ');               // β†’ true   (trimmed, case-insensitive)
const IsBool().isValid('yes');                   // β†’ false

const IsIpAddress().isValid('192.168.1.1');      // β†’ true
const IsIpAddress().isValid('192.168.001.001');  // β†’ false  (leading zeros)

const IsPort().isValid('8080');                  // β†’ true   (0–65535)
const IsPort().isValid('65536');                 // β†’ false

RegExpRule(RegExp(r'^[A-Z]{3}$')).isValid('EGP'); // β†’ true
RegExpRule(RegExp(r'^[A-Z]{3}$')).isValid('usd'); // β†’ false

Lists

const IsIn(['red', 'green', 'blue']).isValid('green');  // β†’ true
const IsIn(['red', 'green', 'blue']).isValid('yellow'); // β†’ false

const IsNotIn(['admin', 'root']).isValid('guest');      // β†’ true
const IsNotIn(['admin', 'root']).isValid('admin');      // β†’ false

const ContainsAny(['http', 'https']).isValid('https://x'); // β†’ true
const ContainsAny(['http', 'https']).isValid('ftp://x');   // β†’ false
const ContainsAny(['USD']).isValid('usd ok');                      // β†’ true   (case-insensitive)
const ContainsAny(['USD'], caseSensitive: true).isValid('usd ok'); // β†’ false

const NotContainsAny(['<', '>']).isValid('safe'); // β†’ true
const NotContainsAny(['<', '>']).isValid('a<b');  // β†’ false

Dates

const IsDate().isValid('2026-06-01'); // β†’ true
const IsDate().isValid('not-a-date'); // β†’ false

const IsDateMillis().isValid('1700000000000'); // β†’ true
const IsDateMillis().isValid('3.14');          // β†’ false

IsDateAfter(DateTime(2020)).isValid('2026-01-01'); // β†’ true
IsDateAfter(DateTime(2020)).isValid('2019-01-01'); // β†’ false

Languages

const IsArabicChars().isValid('Ω…Ψ±Ψ­Ψ¨Ψ§ Ψ¨Ωƒ');  // β†’ true
const IsArabicChars().isValid('Ω…Ψ±Ψ­Ψ¨Ψ§ Ω‘Ω’Ω£'); // β†’ true   (Arabic-Indic digits allowed)
const IsArabicChars().isValid('hello');     // β†’ false

const IsEnglishChars().isValid('Hello');       // β†’ true
const IsEnglishChars().isValid('Hello World'); // β†’ false  (no spaces)
const IsEnglishChars().isValid('abc123');      // β†’ false  (letters only)

const IsNumbersOnly().isValid('12345'); // β†’ true
const IsNumbersOnly().isValid('12a');   // β†’ false

const IsLtrLanguage().isValid('en'); // β†’ true
const IsLtrLanguage().isValid('ar'); // β†’ false

const IsRTLLanguage().isValid('ar'); // β†’ true
const IsRTLLanguage().isValid('en'); // β†’ false

Colors

const IsHexColor().isValid('#FFF');      // β†’ true   (3 digits)
const IsHexColor().isValid('A1B2C3');    // β†’ true   (6 digits, '#' optional)
const IsHexColor().isValid('#FF0000FF'); // β†’ true   (8 digits, with alpha)
const IsHexColor().isValid('red');       // β†’ false

Magic β€” IsOptional

IsOptional only makes sense inside a validator: an empty value skips the rest and passes, but a non-empty value still has to satisfy them.

final validate = xValidator([
  const IsOptional(),
  const IsEmail(),
]);

validate('');       // β†’ null  (empty is allowed)
validate('a@b.co'); // β†’ null  (valid email)
validate('x');      // β†’ IsEmail's message  (non-empty must be valid)

πŸ›  Usage guide

Composing rules

Rules run top-to-bottom and the first failure is returned, so order them from general to specific:

final validate = xValidator([
  const IsRequired(),   // checked first
  const MinLength(8),
  const MaxLength(32),
]);

validate('');        // β†’ IsRequired's message
validate('abc');     // β†’ MinLength's message
validate('hunter2!'); // β†’ null (valid)

Real-world recipes

Ready-to-paste validators for the fields you actually build. Each returns the first failing rule's message, or null when the value passes β€” exactly what TextFormField.validator expects.

Password β€” required, length-bounded, must contain a special character:

final password = xValidator([
  const IsRequired('Password is required'),
  const MinLength(8, 'At least 8 characters'),
  const MaxLength(64, 'At most 64 characters'),
  const ContainsAny(['!', '@', '#', '%'], error: 'Add a special character'),
]);

password('');         // β†’ 'Password is required'
password('abc');      // β†’ 'At least 8 characters'
password('abcdefgh'); // β†’ 'Add a special character'
password('abcdefg!'); // β†’ null (valid)

Email β€” required and well-formed:

final email = xValidator([
  const IsRequired('Email is required'),
  const IsEmail('Enter a valid email'),
]);

email('');             // β†’ 'Email is required'
email('not-an-email'); // β†’ 'Enter a valid email'
email('jane@acme.io'); // β†’ null (valid)

Phone β€” required Egyptian mobile number:

final phone = xValidator([
  const IsRequired('Phone is required'),
  const IsEgyptianPhone('Enter a valid Egyptian number'),
]);

phone('0100');        // β†’ 'Enter a valid Egyptian number'
phone('01012345678'); // β†’ null (valid)

Optional age β€” blank is allowed, but if filled it must be a whole number in range. Put IsOptional first so an empty value skips the rest and passes:

final age = xValidator([
  const IsOptional(),
  const IsNumber('Digits only'),
  const MinValue(18, 'Must be 18 or older'),
  const MaxValue(120, 'Enter a real age'),
]);

age('');   // β†’ null (optional β€” blank is fine)
age('1x'); // β†’ 'Digits only'
age('16'); // β†’ 'Must be 18 or older'
age('30'); // β†’ null (valid)

Optional website β€” blank allowed, otherwise a valid secure URL:

final website = xValidator([
  const IsOptional(),
  const IsUrl('Enter a valid URL'),
  const IsSecureUrl('Use https://'),
]);

website('');                  // β†’ null (optional β€” blank is fine)
website('not a url');         // β†’ 'Enter a valid URL'
website('http://insecure.io'); // β†’ 'Use https://'
website('https://acme.io');    // β†’ null (valid)

Custom error messages

Pass a message as the rule's last argument to override its default:

xValidator([
  const IsRequired('Please enter a value'),
  const MinLength(8, 'Use at least 8 characters'),
]);

Reacting to failures (onFailureCallBack)

Useful for analytics or logging. The callback receives the input, the full rule list, and the rule that failed:

xValidator(
  [
    const IsRequired('Field cannot be empty'),
    const MinLength(3, 'At least 3 characters'),
    const MaxLength(20, 'At most 20 characters'),
  ],
  onFailureCallBack: (input, rules, failedRule) {
    debugPrint('Validation failed for "$input" on ${failedRule.runtimeType}');
  },
);

Optional fields

Add IsOptional to let an empty field pass while still validating non-empty input. Here an empty value is accepted, but anything typed must be a valid email:

xValidator([
  const IsOptional(),
  const IsEmail(),
]);

Writing a custom rule

Extend TextXValidationRule, implement isValid, and (optionally) override defaultMessage to provide a default message:

class StartsWithCapital extends TextXValidationRule {
  const StartsWithCapital([super.error]);

  @override
  bool isValid(String input) =>
      input.isNotEmpty && input[0] == input[0].toUpperCase();

  @override
  String get defaultMessage => 'Must start with a capital letter';
}

// Use it like any built-in rule:
xValidator([const StartsWithCapital()]);

Overriding toString() still works β€” the base defaultMessage delegates to it β€” but defaultMessage is the preferred hook for a rule's default message.

Localizing error messages

Register a translator per rule type with XValidatorsLocalization.on<T>(). When that rule fails (and no inline error was given), your function supplies the message:

XValidatorsLocalization.on<IsRequired>((rule) => 'Ω‡Ψ°Ψ§ Ψ§Ω„Ψ­Ω‚Ω„ Ω…Ψ·Ω„ΩˆΨ¨');
XValidatorsLocalization.on<IsEmail>((rule) => 'Ψ§Ω„Ψ¨Ψ±ΩŠΨ― Ψ§Ω„Ψ₯Ω„ΩƒΨͺΨ±ΩˆΩ†ΩŠ غير Ψ΅Ψ§Ω„Ψ­');

final validate = xValidator([const IsRequired()]);
validate(''); // β†’ 'Ω‡Ψ°Ψ§ Ψ§Ω„Ψ­Ω‚Ω„ Ω…Ψ·Ω„ΩˆΨ¨'

Resolution order for a failing rule is: inline error β†’ registered translator β†’ rule.defaultMessage.

Standalone helper functions

Each rule also ships a top-level function for one-off checks outside of a form, mirroring the rule's logic:

isNotEmpty('hello');     // true
isEmpty('   ');          // true
minLength('abc', 3);     // true
EmailXValidator.validate('test@example.com'); // true

ℹ️ Good to know

  • A null value passed to the validator is treated as an empty string '', so IsRequired (and every other rule) sees '' and a required field correctly fails on null. Add an IsOptional rule if you want an empty/null value to skip the remaining rules and pass. (Before 2.0.0, null short-circuited the whole validator to "valid" β€” see Migrating to 2.0.0.)

Migrating to 2.0.0

2.0.0 is a behavior-only major release: the public API shape is unchanged (same rules, same xValidator signature), but several rules were tightened to do what their names promise. Most apps need no code changes β€” the table below lists every behavior change and how to restore the old behavior where it's recoverable.

Area Before (1.x) After (2.0.0) How to adapt
null input Short-circuited the whole validator to valid Treated as '', so IsRequired rejects it Add IsOptional to let empty/null pass
IsNumber Accepted decimals, hex, scientific (3.14, 1e3, 0x1A) Integers only Use the new IsDecimal for fractional values
IsArabicChars \p{N} token matched the literal chars p{N} and no digits Accepts Arabic-Indic digits Ω -Ω© only β€” (this was a bug; Latin 123 was never really allowed)
IsNumbersOnly Passed if the input contained a digit ('abc1', '12 34') Passes only when the input is all digits Use Contains/RegExpRule for "contains a digit"
IsFacebookUrl / IsInstagramUrl / IsYoutubeUrl Unanchored β€” facebook.com.evil.com passed Fully anchored β€” domain-suffix spoofing is rejected β€” (intended hardening)
IsUrl Rejected hyphens, deep subdomains, long TLDs Accepts my-site.co.uk, a.b.example.com, example.museum β€” (relaxation only)
IsIpAddress Tolerated leading zeros and whitespace (192.168.001.001, ' 1.2.3.4') Strict dotted-quad Trim/normalize before validating if needed
ContainsAny error was positional; caseSensitive was a dead no-op error is named; caseSensitive works See snippet below
IsArabicNum / IsHindiNum Both used key validation.must_be_num validation.must_be_arabic_num / validation.must_be_hindi_num Re-key your translators (inline error: is unaffected)
isInstgramUrlValid Typo'd free function Renamed to isInstagramUrlValid Old name still works as a @Deprecated alias

Decimals: IsNumber β†’ IsDecimal

// 1.x β€” accepted "3.14"
xValidator([const IsNumber()]);

// 2.0.0 β€” integers only; use IsDecimal for fractional input
xValidator([const IsDecimal()]);

ContainsAny: error is now named, caseSensitive now works

// 1.x
const ContainsAny(['a', 'b'], 'Pick one');          // positional error
final r = ContainsAny(['a'])..caseSensitive = true; // dead no-op field

// 2.0.0
const ContainsAny(['a', 'b'], error: 'Pick one');   // named error
const ContainsAny(['a'], caseSensitive: true);      // actually case-sensitive

If you imported individual rule files directly, note two internal renames: text/is_not_empty.dart β†’ text/is_required.dart and urls/is_instgram_url.dart β†’ urls/is_instagram_url.dart. Importing the package barrel (package:x_validators/x_validators.dart) needs no change.

πŸ“„ License

See LICENSE.

πŸ‘¨πŸ»β€πŸ’» Author

Mahmoud Basuony Mahmoud Basuony
Software Engineer

If x_validators saved you some boilerplate, a ⭐ on the repo is appreciated.

About

β˜• πŸš€ X Validators is a library that simplifies data validation in your applications.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages