Phone topic

MREPhoneField is an ordinary phone input. MREPhoneFormField adds built-in localized validation, structured onSaved and country-plus-digits reset. Both accept decoration, controller, country selection and presentation options.

A field

The country button shows the flag and the dial code. Tap it to pick another country. number.e164 is the number to save or send.

final field = MREPhoneFormField(
  labelText: 'Phone number',
  onChanged: (number) {
    if (number.isValid) {
      saveNumber(number.e164!); // '+201012345678'
    }
  },
);

What it does for the user

The user... The field...
Types 1012345678 or 01012345678 Reads it as a number of the chosen country
Pastes +20 (10) 1234-5678 Switches to Egypt and fills 1012345678
Pastes 0020 101 234 5678 The same: 00 is read as +
Types digits in Arabic (٠١٠١٢٣٤٥٦٧٨) Converts them to ASCII digits
Pastes a +1 204… number Finds Canada, not the United States: the whole number decides
Pastes a number from an excluded country Keeps it and says the country is not accepted
Taps the country button Opens a searchable list: a sheet on narrow windows, a dialog on wide ones

Choose the countries

One selection feeds the picker, the paste detection and the validation, so an excluded country cannot get in by any path.

const selection = MRECountrySelection(
  include: {'SA', 'AE', 'KW', 'QA', 'BH', 'OM'},
  favorites: ['SA', 'AE'],
  initial: 'SA',
);

final field = MREPhoneFormField(selection: selection);

Or exclude some:

final field = MREPhoneFormField(
  selection: const MRECountrySelection(exclude: {'IL'}),
);

Give either include or exclude, not both. favorites are pinned at the top of the picker. initial is the country a new field starts with; without it the field uses the device country when it is accepted, then the first favorite, then the first accepted country.

Read the number

MREPhoneNumber splits what was typed. It works without any widget.

final number = MREPhoneNumber.parse('+20 101 234 5678');

final country = number.country!.name; // 'Egypt'
final e164 = number.e164; // '+201012345678'
final nice = number.international; // '+20 10 12345678'
final valid = number.isValid; // true

Numbers without a dial code need a country:

final egypt = MRECountries.byIsoCode('EG')!;
final number = MREPhoneNumber.parse('010 1234 5678', defaultCountry: egypt);

final e164 = number.e164; // '+201012345678'

Why a number is not valid

When isValid is false, error says why. The field shows the message and you can show your own.

MREPhoneError Meaning
empty Nothing was entered
tooShort Fewer digits than any number of the country has
tooLong More digits than any number of the country has
invalid The length fits but the number matches no pattern
unknownCountry The dial code after the plus belongs to no country
countryNotAllowed The number is from a country the selection does not accept
missingCountry A national number and no country to read it in
final number = MREPhoneNumber.parse(
  typed,
  defaultCountry: MRECountries.byIsoCode('EG'),
);

final error = number.error; // null, or tooShort, tooLong, invalid, ...

Translate it

Country names, the picker texts and every error message can be in your language. Names come from countryNameBuilder (English is the fallback). Error messages come from MREFieldsStrings, registered once on your theme.

final field = MREPhoneFormField(
  labelText: 'رقم الهاتف',
  countryNameBuilder: (country) =>
      arabicNames[country.isoCode] ?? country.name,
  pickerTitle: 'اختر الدولة',
  pickerSearchHint: 'ابحث عن دولة أو رمز',
);

// The error messages come from MREFieldsStrings, registered once in the theme:
const strings = MREFieldsStrings(
  phoneEmpty: 'اكتب رقم الهاتف',
  phoneTooShort: 'الرقم قصير',
  phoneInvalid: 'رقم غير صحيح',
);

Get the message for an error yourself:

const strings = MREFieldsStrings();
final message = strings.phoneError(error); // 'The phone number is too short'

Use it in a form

final form = Form(
  key: formKey,
  child: MREPhoneFormField(labelText: 'Phone number', required: true),
);

Without a parent Form, pass fieldKey and call fieldKey.currentState!.validate().

Validate any field

The same validation works in a plain TextFormField:

final field = TextFormField(
  keyboardType: TextInputType.phone,
  validator: MREPhoneValidators.valid(
    country: MRECountries.byIsoCode('EG'),
    selection: const MRECountrySelection(include: {'EG', 'SA'}),
  ),
);

Read and set the number from code

// Create the controller in initState and dispose it in dispose.
final field = MREPhoneFormField(controller: controller, labelText: 'Phone');

controller.setNumber('+966 50 123 4567'); // sets the country and the digits
final number = controller.number(); // an MREPhoneNumber

The controller notifies when the country changes. Listen to controller.text for typing.

Countries

final egypt = MRECountries.byIsoCode('EG')!;
final dial = egypt.dialCodeWithPlus; // '+20'
final flag = egypt.flag; // 🇪🇬
final sharing = MRECountries.byDialCode('+1'); // United States, Canada, ...

A dial code can belong to several countries, so the ISO code is the key: two countries are equal when their ISO codes are.

The picker on its own

final country = showMRECountryPicker(
  context,
  countries: MRECountries.all,
  favorites: [MRECountries.byIsoCode('EG')!],
);

MRECountryPickerBody is the list without the sheet or dialog, if you want to put it in your own page.

How the numbers are checked

The rules come from the libphonenumber data through the phone_numbers_parser package: length and pattern for 245 countries and regions. The package hides it behind MREPhoneNumber, so your code does not depend on it.

Country detection while typing

Typing +20 or 0020 immediately selects Egypt, even before the rest of the number is valid. Country detection and validation are independent. Shared dial codes such as +1 initially use the parser's provisional region and can change as more digits identify the destination (for example, Canada).

Typed international text is retained until valid so the destination can still be refined. A pasted number is split immediately. Unknown or excluded countries do not replace the current selection. onCountryChanged fires only on a real country change.

The dial button listens only to country changes. Repeated reads of the same controller text, country and country selection reuse the parsed result, including form validation reads. Text beginning with + or 00 is validated as an international number even while its prefix is visible in the editor.

Classes

MRECountries Phone
Every country with a dial code, and lookups.
MRECountry Phone
A country or region with a phone dial code.
MRECountryPickerBody Phone
A searchable list of countries: a title, a search field and the countries.
MRECountrySelection Phone
Which countries a phone field accepts, shows first, and starts with.
MREPhoneController Phone
Holds the country and the national number of an MREPhoneField.
MREPhoneField Phone
A phone number field: a country button with the dial code, and the number.
MREPhoneFormField Phone
A phone form field with a structured MREPhoneNumber value.
MREPhoneInputFormatter Phone
Keeps a phone field to digits and a leading plus, whatever is typed or pasted. See mreNormalizePhoneInput.
MREPhoneNumber Phone
A phone number split into its country and national number.
MREPhoneValidators Phone
Validators for phone numbers, for any form field.

Extensions

MREPhoneErrorText on MREFieldsStrings Phone
The text of a phone error, taken from MREFieldsStrings.

Functions

showMRECountryPicker(BuildContext context, {required List<MRECountry> countries, MRECountry? selected, List<MRECountry> favorites = const [], String? title, String? searchHint, String? emptyText, String nameBuilder(MRECountry country)?, bool showFlags = true}) → Future<MRECountry?> Phone
Opens the country picker and returns the country the user chose, or null when it is dismissed.

Enums

MREPhoneError Phone
Why a phone number is not valid.