country_mobile_validator

architecture

Validate mobile numbers per country with the real, actual length ranges — works out-of-the-box with country_code_picker.

country_mobile_validator is the missing piece in the Dart ecosystem: existing phone packages validate "any phone number" against frozen metadata, return booleans, and can't tell you that a number is a mobile number (or that Argentina allows 10–11 digits while New Zealand allows 8–10).

This library:

  • ✅ Validates mobile numbers only (OTP-ready) — rejects landlines, toll-free, premium-rate, and short codes.
  • Range-aware: every country exposes its real mobile length range (8–10, 10–11, …) so you never hardcode "10 digits" again.
  • country_code_picker-friendly: pick a country, validate the number against that country's actual mobile length range — no second library.
  • Refreshable metadata (SHA-256 verified, offline fallback) — bundled data from libphonenumber (Apache-2.0), updatable at runtime.
  • ✅ Pure Dart core — works on Flutter, Dart VM, and web. ~8 µs per validation, no platform channels.
  • ✅ Handles +, separators, Unicode digits, extensions, trunk prefixes, and shared country codes (+1 → US/CA/…, +44 → GB/GG/JE/…).

Quick start

import 'package:country_mobile_validator/country_mobile_validator.dart';

final kit = MobileNumberKit();

// Smart: auto-detects country from the calling code.
final r = kit.validate('+919876543210');          // India
print(r.isValid);      // true
print(r.isMobile);     // true
print(r.regionCode);   // IN

// Pinned: validate in national format against a specific country.
final v = kit.forRegion('AR');                    // Argentina
print(v.validate('91123456789').isValid);         // true (11 digits)
print(v.validate('911234567').isValid);           // false (too short)
print(v.lengthHint);                              // "10–11 digits"

Features at a glance

Feature API What it gives you
Per-country mobile length ranges forRegion('NZ').mobileLengthRange(8, 10) The real allowed lengths — never hardcoded "10 digits"
Live length check forRegion('AR').isMobileLength(9)false Real-time feedback while typing
Range error message forRegion('BR').lengthHint"10–11 digits" User-facing "enter 10–11 digits" hints
Pinned validation validateForCountry('IN', '9876543210') country_code_picker-style flow (see below)
Auto-detection validate('+1 415 555 2671') Region resolved from the calling code (+1 → US/CA…)
Mobile-only verdict result.isMobile, result.isOtpDeliverable OTP-safe: toll-free/premium/landline rejected
Type awareness result.typetollFree/premiumRate/… Know what the number is, not just if it's "valid"
Rejection reason result.issuetooShort/invalidPrefix/… Why it failed — great for inline form errors
Fresh metadata MetadataUpdater + result.metadataVersion SHA-256 verified updates, offline fallback
Input hygiene separators, Unicode digits, extensions +91 (98765) 43210 x2 parses cleanly

▶ Run the live demo: example/ — a complete Flutter app wiring this library to country_code_picker with live range feedback and OTP-safe verdicts (cd example && flutter run).

country_code_picker integration (the workflow this library was built for)

Use country_code_picker for the flag/dial-code picker, then validate against that country's actual mobile length range — the picker gives you Country.countryCode ('IN', 'US'…) and Country.dialCode ('+91'…). No second library needed:

import 'package:country_code_picker/country_code_picker.dart';
import 'package:country_mobile_validator/country_mobile_validator.dart';

final kit = MobileNumberKit();
Country? selected;

CountryCodePicker(
  onChanged: (Country c) => selected = c,
)

// On submit:
void submit(String input) {
  final v = kit.forRegion(selected!.countryCode);     // e.g. 'IN'
  final res = v.validate(input);                       // national format
  if (!res.isValid) {
    // Error message uses the REAL range, e.g. "Enter a valid Indian mobile
    // number (10 digits)" — never a hardcoded length.
    error = 'Enter a valid mobile number (${v.lengthHint})';
  }
}

Live length feedback while typing (still valid for AR 10–11, NZ 8–10…):

controller.addListener(() {
  final v = kit.forRegion(selected!.countryCode);
  final len = controller.text.replaceAll(RegExp(r'\D'), '').length;
  setState(() => hint = v.isMobileLength(len) ? '✓' : 'Needs ${v.lengthHint}');
});

Or use the convenience one-liner:

final res = kit.validateForCountry(selected!.countryCode, input);

The range API (the differentiator)

final v = kit.forRegion('NZ');                    // New Zealand
print(v.mobileLengthRange);  // (8, 10)
print(v.lengthHint);         // "8–10 digits"
print(v.isMobileLength(8));  // true
print(v.isMobileLength(7));  // false

Length variance is real: AR 10–11, NZ 8–10, BR 10–11, ID 9–12, DE 10–11, GB 10, IN 10, US 10. country_mobile_validator knows them all.

Type awareness (OTP-safe verdicts)

final r = kit.validate('+18005551234');           // US toll-free
print(r.type);            // NumberType.tollFree — NOT mobile
print(r.isOtpDeliverable);// false — don't send an OTP here

A 800-number will never pass as a mobile. Types: mobile, fixedLine, tollFree, premiumRate, sharedCost, shortCode, unknown.

Refreshable metadata ("real-time")

Bundled data is versioned. Update it at runtime with SHA-256 verification:

final updater = MetadataUpdater(
  manifestUrl: 'https://example.com/mobile-num-metadata/manifest.json',
);
final res = await updater.checkAndUpdate(currentVersion: kit.metadataVersion);
if (res.success && res.newVersion != null) {
  kit.loadRefreshedMetadata(updater.lastVerifiedJson!, version: res.newVersion!);
} else {
  // Offline fallback: keep using the bundled snapshot. Nothing breaks.
}

On web, inject your own fetcher (e.g. package:http):

MetadataUpdater(
  manifestUrl: '...',
  fetcher: (url) async => (await http.get(Uri.parse(url))).bodyBytes,
);

The manifest is a tiny JSON:

{ "version": "2026.08", "sha256": "…", "url": "…" }

Region detection with shared country codes

+1 covers 25 NANP regions; +44 covers GB, GG, JE, IM. The library resolves by longest-prefix match, tries the main region first, then the others, and returns the region whose mobile pattern actually matches:

kit.validate('+14155552671').regionCode;   // US (415 = San Francisco)
kit.validate('+14165551234').regionCode;   // CA (416 = Toronto)

What it handles

Input Behavior
+91 98765 43210 separators stripped
٠٩٨٧٦٥٤٣٢١٠ Arabic-Indic digits normalized
+1 415 555 2671 x123 extension split off, ignored
011 91 98765 43210 011 international prefix handled
+1… vs +44… longest-prefix calling-code match
9876543210 (IN) region-pinned national format

Development

dart pub get
dart test          # 37 tests: golden corpus over all 247 mobile regions + updater
dart analyze
dart run benchmark/bench.dart

Metadata refresh pipeline:

python3 tools/extract_metadata.py   # libphonenumber XML → tools/data/regions.json
python3 tools/gen_dart_data.py      # JSON → lib/src/data/regions_data.dart

License & attribution

Apache-2.0. Metadata is generated from libphonenumber (Apache-2.0, Copyright Google Inc.). Validators are compared against libphonenumber's example corpus on every test run.

Libraries

country_mobile_validator
country_mobile_validator — validate mobile numbers per country with real length ranges (8-10, 10-11 digits...), mobile-only type detection, and a country_code_picker-friendly API.