phone_number_form_field 0.2.0
phone_number_form_field: ^0.2.0 copied to clipboard
International phone number field with a dial code picker, as-you-type formatting and E.164 validation
phone_number_form_field #
An accessible, international phone number input field for Flutter applications featuring intelligent country dial-code selection, as-you-type formatting, and rigorous international E.164 compliance validation.
PhoneNumberFormField delivers an enterprise-grade phone input experience by uniting an interactive calling-code prefix selector with continuous formatting and parsing powered by Google's authoritative libphonenumber metadata.
| Phone Number | Language Switcher |
|---|---|
Key Architectural Strengths #
- Powered by Google's
libphonenumberStandard (dlibphonenumber):- Employs the industry gold standard for international phone number plans, area codes, number lengths, and formatting rules across every global region.
- Avoids brittle regexes or crude length assumptions that fail on complex national numbering systems.
- Continuous As-You-Type Formatting with Trunk Stripping:
- Formats numbers naturally in real time as user digits are typed.
- Automatically detects and removes redundant domestic trunk prefixes (such as the leading
0in UK or Australian mobile numbers) when typed or pasted, guaranteeing proper international formatting.
- Strict International E.164 State Contract:
- Emits an immutable
PhoneNumberStatecontainingrawText,regionCode, and parsede164(e.g.+442079460123). e164is non-null only when the number is genuinely valid and deliverable, making backend integrations (Twilio, AWS SNS, Firebase Auth, Stripe) immediate and reliable.
- Emits an immutable
- Modern Material 3 Prefix Selector with Typo-Tolerant Search:
- The dial code selector uses a Material 3
SearchAnchoroverlay that opens a fast, in-memory fuzzy search over country names, ISO codes, and dialing prefixes.
- The dial code selector uses a Material 3
- Multi-Lingual Country Search:
- Country names and search aliases are translated across 39 global languages (English, French, German, Spanish, Welsh, Japanese, Chinese, Arabic, Russian, Korean, Turkish, Hindi, and more), resolved synchronously from the active
Localizationslocale.
- Country names and search aliases are translated across 39 global languages (English, French, German, Spanish, Welsh, Japanese, Chinese, Arabic, Russian, Korean, Turkish, Hindi, and more), resolved synchronously from the active
- Lightweight, Zero-Asset Design:
- Renders native Unicode flag emojis without bundling hundreds of heavy vector or raster flag image files.
- Fully offline, self-contained, and tested with >99% code coverage.
Features #
- Country Calling-Code Selector: Tap prefix to search all 249 ISO countries by localized name, code, or dial prefix.
- As-You-Type Phone Formatting: Automatic national phone structure formatting.
- Trunk Code Normalization: Automatically strips redundant domestic trunk zeros.
- E.164 Compliance Validation: Instant access to formatted E.164 string and boolean validity checks (
state.isValid). - Flexible Form Integration: Drop-in
Formsupport through typedvalidatorandonSavedcallbacks that receive the parsedPhoneNumberState, alongsideonPhoneNumberChangedforFormzand BLoC/Riverpod state managers.
Getting Started #
Add phone_number_form_field and saible_consulting_core to your pubspec.yaml:
dependencies:
phone_number_form_field: ^0.2.0
saible_consulting_core: ^0.2.0
UI library: this package is built on
material_ui, the official Flutter Material library, and its API acceptsmaterial_uitypes such asInputDecoration. Importpackage:material_ui/material_ui.dartin code that constructs those arguments: it exports distinct types that are not assignable to or from the copies exported bypackage:flutter/material.dart.
Localization setup #
Configure your MaterialApp with the localization plumbing from
saible_consulting_core (no provider or lookup is required):
import 'package:material_ui/material_ui.dart';
import 'package:saible_consulting_core/saible_consulting_core.dart';
MaterialApp(
supportedLocales: SaibleLocalizations.supportedLocales,
localizationsDelegates: SaibleLocalizations.localizationsDelegates,
home: const MyPhoneFormPage(),
);
Migrating from package:flutter/material.dart #
material_ui is the Material library that used to ship inside
package:flutter/material.dart, so migration is mostly mechanical - the bundled
data-driven fix rewrites the imports for you:
dart fix --apply --code=migrate_design_widgets
Two things then need attention. Import package:material_ui/material_ui.dart
wherever you construct material_ui types (such as InputDecoration), as
described above - and make sure the localization delegates come from
material_ui too. material_ui widgets never read the legacy
GlobalMaterialLocalizations, GlobalCupertinoLocalizations or
GlobalWidgetsLocalizations classes from package:flutter_localizations, which
is why SaibleLocalizations.localizationsDelegates (shown above) pairs the
country names with material_ui's own Material, Cupertino and Widgets
delegates. Use the generated CountryLocalizations.localizationsDelegates only
while parts of your app still build with package:flutter/material.dart.
If a dependency or subtree still imports package:flutter/material.dart, use
MaterialUiCompatibilityBridge to bridge ThemeData and
MaterialLocalizations for it - app-wide through MaterialApp.builder, or
around the individual subtree:
MaterialApp(
builder: (context, child) => MaterialUiCompatibilityBridge(child: child!),
home: const MyScreen(),
);
The bridge is a temporary migration aid (deprecated in material_ui 1.5.0 and
scheduled for removal in a future release), so migrate the legacy dependency
rather than shipping it long term.
Usage #
Basic Example #
import 'package:material_ui/material_ui.dart';
import 'package:phone_number_form_field/phone_number_form_field.dart';
class PhoneInputExample extends StatefulWidget {
const PhoneInputExample({super.key});
@override
State<PhoneInputExample> createState() => _PhoneInputExampleState();
}
class _PhoneInputExampleState extends State<PhoneInputExample> {
PhoneNumberState _phoneState = const PhoneNumberState.empty();
@override
Widget build(BuildContext context) {
return PhoneNumberFormField(
decoration: InputDecoration(
labelText: 'Phone Number',
border: const OutlineInputBorder(),
errorText: _phoneState.isNotEmpty && !_phoneState.isValid
? 'Please enter a valid phone number'
: null,
),
onPhoneNumberChanged: (state) {
setState(() {
_phoneState = state;
});
print('E.164 number: ${state.e164}'); // e.g. "+442079460123"
print('Region: ${state.regionCode}'); // e.g. "+44"
print('Valid: ${state.isValid}');
},
);
}
}
Prepopulating an Initial Number #
You can initialize the field with an existing PhoneNumberState:
PhoneNumberFormField(
initialValue: const PhoneNumberState(
rawText: '20 7946 0123',
e164: '+442079460123',
regionCode: '+44',
),
onPhoneNumberChanged: (phoneState) {
// handle state changes
},
);
Form Validation #
validator and onSaved plug the field into a Form and receive the parsed
PhoneNumberState, so validation and submission work on the E.164 value and the
selected dial code rather than on the raw text:
final formKey = GlobalKey<FormState>();
PhoneNumberState? saved;
Form(
key: formKey,
child: PhoneNumberFormField(
decoration: const InputDecoration(labelText: 'Phone Number'),
validator: (state) => state.isValid ? null : 'Enter a valid phone number',
onSaved: (state) => saved = state,
),
);
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
print(saved!.e164); // e.g. "+442079460123"
print(saved!.regionCode); // e.g. "+44"
}
The validator runs whenever the enclosing Form validates - through
FormState.validate() or a form-level AutovalidateMode. state.e164 is
null until the number is complete and valid, so a single state.isValid check
covers empty, partial and malformed input; inspect state.rawText instead when
you want to distinguish "required" from "malformed".
Testing #
PhoneNumberFormField exposes stable testing keys for automation:
PhoneNumberFormField.countrySearchAnchorKey: Key for the prefix search anchor.PhoneNumberFormField.countrySearchBarKey: Key for the phone number text entry field.PhoneNumberFormField.countrySuggestionKey(country): Generates a test key for a country's suggestion tile.
Example widget test:
// Enter phone number
await tester.enterText(
find.byKey(PhoneNumberFormField.countrySearchBarKey),
'02079460123',
);
await tester.pumpAndSettle();
// Tap dial code selector
await tester.tap(find.text('+44'));
await tester.pumpAndSettle();
How this compares #
Alternative pub.dev packages, with their like counts, 30-day downloads and latest releases as of 1 October 2026:
| Package | Likes | Downloads | Latest | Trade-offs |
|---|---|---|---|---|
intl_phone_number_input |
926 | 128k | 0.7.5 (Sep 2025) | The most-used option and also built on dlibphonenumber. Its InternationalPhoneNumberInput offers selector styles (dropdown, bottom sheet, dialog), but it asks you to assemble more of the field yourself, and its flags are image assets unless you opt into emoji. Published by an unverified uploader. |
intl_phone_field |
782 | 119k | 3.2.0 (Jun 2023) | No release since 2023, so its validation rules are frozen rather than tracking current numbering plans. |
phone_number_field |
6 | 94 | 1.2.0 (Jul 2025) | Minimal: English-only country names and regex-based validation. |
Pick phone_number_form_field when you want libphonenumber-grade parsing and an
E.164 result from a single widget that drops straight into a Form, with an
offline dial code picker that fuzzy-searches all 249 countries in 39 languages
and renders Unicode flag emojis instead of bundling flag assets.
Additional Information #
- Source code: GitHub Repository
- Issue tracker: File bugs or feature requests via GitHub Issues.
- License: See LICENSE for details.