flutter_country_phone

Country catalog, localized country picker, SVG flags, and international phone form fields for Flutter apps.

flutter_country_phone gives you a reusable foundation for forms that need a country selector, a phone input with country prefix handling, or a simple way to render country names and flags. It ships with its own static country catalog and flag assets, so your app does not need to duplicate those resources.

Features

  • Static country catalog with ISO 3166-1 alpha-2 codes, phone prefixes, and localized country names.
  • Package-owned SVG flags through CountryFlag.
  • Drop-in widgets: no app-level provider is required.
  • Optional CountryPhoneScope to share one CountryBloc across many fields when you want explicit catalog reuse.
  • CountryFormField for selecting a country inside a Form.
  • PhoneFormField for collecting an international phone number.
  • CountryField and PhoneField lower-level widgets when you do not need a FormField wrapper.
  • Search by localized country name, ISO code, or phone prefix.
  • onlyCountries filtering for restricted country lists.
  • CountryPhoneFieldStyle plus item/button builders for UI customization.
  • Locale fallback support for values such as es, es_MX, es-MX, pt_BR, and pt-BR.

Installation

Add the package to your pubspec.yaml:

dependencies:
  flutter_country_phone: ^0.1.2

The package declares its own flag assets. No extra asset registration is needed in your app.

Basic usage

Import the package and use the widgets directly. The country catalog provider is created internally when one is not already available in the widget tree:

import 'package:flutter_country_phone/flutter_country_phone.dart';

PhoneFormField(
  decoration: const InputDecoration(labelText: 'Phone number'),
  onChanged: (value) {
    debugPrint(value); // Example: +15551234567
  },
)

If several country and phone widgets live together and you want them to share a single in-memory catalog, wrap that small subtree with CountryPhoneScope:

CountryPhoneScope(
  child: Column(
    children: [
      CountryFormField(
        decoration: const InputDecoration(labelText: 'Country'),
      ),
      PhoneFormField(
        decoration: const InputDecoration(labelText: 'Phone number'),
      ),
    ],
  ),
)

Apps that already provide CountryBloc continue to work. The package will reuse the existing bloc instead of creating another one.

Country picker

Use CountryFormField when the selected country is part of a Form:

CountryFormField(
  value: 'US',
  decoration: const InputDecoration(
    labelText: 'Country',
    hintText: 'Select your country',
  ),
  validator: (country) {
    if (country == null) return 'Select a country';
    return null;
  },
  onChanged: (country) {
    debugPrint(country?.iso2); // US
  },
)

Restrict the list when a flow supports only a few countries:

CountryFormField(
  onlyCountries: const ['US', 'BR', 'ES'],
  showPhoneCode: true,
  onChanged: (country) {
    debugPrint('${country?.iso2} ${country?.phoneCode}');
  },
)

If you need a plain widget instead of a form field, use CountryField:

CountryField(
  decoration: const InputDecoration(labelText: 'Country'),
  locale: 'pt-BR',
  onChanged: (country) {
    debugPrint(country.iso2);
  },
)

International phone field

PhoneFormField lets the user select a country first and then enter the national phone number. The onChanged value is the international number string generated by phone_numbers_parser.

PhoneFormField(
  decoration: const InputDecoration(
    labelText: 'Phone number',
    hintText: 'Enter your phone number',
  ),
  textInputAction: TextInputAction.next,
  autofillHints: const [AutofillHints.telephoneNumber],
  validator: (value) {
    if (value == null || value.isEmpty) return 'Enter a phone number';
    return null;
  },
  onChanged: (value) {
    debugPrint(value); // Example: +15551234567
  },
)

You can pass an existing international number as the initial value:

PhoneFormField(
  initialValue: '+5511999999999',
  decoration: const InputDecoration(labelText: 'Phone'),
)

When the initial value is parseable and its country is allowed, the field opens directly in phone-entry mode with the matching country selected.

Rendering countries and flags

Use CountryFlag when you only need a flag:

const CountryFlag(
  isoCode: 'BR',
  size: 24,
  borderRadius: BorderRadius.all(Radius.circular(4)),
)

Use CountryWidget when you need the localized country name with the flag:

const CountryWidget(
  iso: 'ES',
  flagSize: 18,
)

CountryWidget is also drop-in and loads the catalog automatically when needed.

Localization

Widgets use Localizations.localeOf(context) by default. You can override the locale when a field needs to render in a specific language:

CountryFormField(
  locale: 'es-MX',
  decoration: const InputDecoration(labelText: 'PaĆ­s'),
)

Locale values can be language-only (es) or region-specific (es_MX, es-MX, pt_BR, pt-BR). The lookup tries the full locale, then the language code, then English, then the first non-empty available value.

You can also customize labels with a builder:

CountryFormField(
  countryLabelBuilder: (context, country) {
    return country.localizedName('pt-BR');
  },
)

For app-specific empty states, pass emptyBuilder.

Styling

Most visual customization is intentionally grouped into CountryPhoneFieldStyle:

const countryPhoneStyle = CountryPhoneFieldStyle(
  overlayMaxHeight: 260,
  overlayBorderRadius: BorderRadius.all(Radius.circular(18)),
  overlayElevation: 10,
  overlayPadding: EdgeInsets.all(8),
  itemPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
  itemBorderRadius: BorderRadius.all(Radius.circular(14)),
  flagSize: 24,
  selectedFlagSize: 20,
  flagFit: BoxFit.cover,
);

CountryFormField(
  style: countryPhoneStyle,
)

For deeper customization, replace the country item or selected-country button:

PhoneFormField(
  countryItemBuilder: (context, country, selected) {
    return ListTile(
      leading: CountryFlag(isoCode: country.iso2, size: 22),
      title: Text(country.localizedName('es')),
      trailing: Text(country.phoneCode),
      selected: selected,
    );
  },
  selectedCountryButtonBuilder: (context, country, onPressed) {
    return TextButton.icon(
      onPressed: onPressed,
      icon: CountryFlag(isoCode: country.iso2),
      label: Text(country.phoneCode),
    );
  },
)

Working with the catalog

You can use the catalog directly through the datasource/use case stack:

final bloc = CountryBloc();

await bloc.fetchCountries();

final countries = bloc.countries;
final unitedStates = countries.firstWhere((country) => country.iso2 == 'US');

CountryBloc keeps the loaded countries in memory. Calling fetchCountries() again will reuse the current list unless force: true is passed internally by refreshCountries().

For UI code, prefer the widgets directly. Reach for CountryBloc only when you need programmatic access to the catalog or explicit sharing through CountryPhoneScope.

API notes

  • CountryEntity is immutable, extends Equatable, and includes copyWith.
  • CountryModel extends CountryEntity and provides fromJson, fromEntity, and toJson.
  • CountryModel.fromJson accepts phoneCode or phone_code and normalizes ISO codes to uppercase.
  • CountryFormField, CountryField, PhoneFormField, PhoneField, and CountryWidget automatically provide a CountryBloc when no ancestor bloc is present.
  • CountryPhoneScope is available when a subtree should share one explicit country catalog instance.
  • Phone formatting and parsing are delegated to phone_numbers_parser.
  • The package is UI-framework friendly: use the widgets directly or keep the domain/data classes for app-specific wrappers.

Development

Run the local checks before publishing:

flutter analyze
flutter test
flutter pub publish --dry-run

License

MIT License. See LICENSE.