countrify_light 1.0.1 copy "countrify_light: ^1.0.1" to clipboard
countrify_light: ^1.0.1 copied to clipboard

Offline Flutter country, state, and city pickers with emoji flags, localized names, and compact bundled geo data.

Countrify Light #

Offline Country, State, and City Pickers for Flutter #

Configurable country, phone-code, state, and city fields with bundled data and platform emoji flags. No network lookups or third-party runtime packages.

pub package Code: MIT Data: ODbL 1.0 Flutter Dart

pub.flutter-io.cn | GitHub | Upstream | Example

Independently maintained from Arhamss/countrify, with its own package identity, compact geographic data, and widget fixes. Original code and data attribution are preserved in License.


Table of Contents #


Overview #

Countrify Light provides offline country, state, and city selection for Flutter. It ships with 250 country/territory records (249 officially assigned ISO 3166-1 entries plus XK/Kosovo), 132 language maps, 5 presentation options, 3 theme presets plus a custom theme builder, utility methods, and a phone number input field, with no third-party runtime packages.

Metric Value
Country/territory records 250 (249 ISO 3166-1 + XK/Kosovo)
States / provinces 5,308
Cities 152,970
Language Translations 132 (CLDR-based)
Flag Assets None (platform emoji)
Utility Methods 40+
Presentation Bottom sheet, dialog, full screen, dropdown, embedded list (varies by widget)
Themes Light, dark, Material 3, and a custom color builder
Runtime Dependencies Flutter SDK only
Platforms iOS, Android, Web, macOS, Windows, Linux

Key Features #

  • Lightweight Emoji Flags — platform emoji flags without bundled image assets
  • Flexible Presentation — Bottom sheet, dialog, full screen, dropdown, and embedded country lists
  • Theming — Default (light), dark, Material 3, and a custom color builder
  • PhoneNumberField — Phone text input with an integrated calling-code picker and your own validation
  • CountryDropdownField — Form-friendly dropdown with InputDecoration support
  • Real-Time Search — Debounced search across name, code, capital, region, and phone code
  • Country Filtering — Country/region allowlists and exclusions, independence status, and UN membership
  • Custom Sorting — Sort by name, population, area, region, or capital
  • Flag Customization — Rectangular, circular, or rounded shapes with borders and shadows
  • Custom Builders — Provide your own widgets for country items, headers, search bars, and filters
  • Customizable Strings — Shared UI text via CountryPickerConfig, plus comprehensive filter labels via widget parameters
  • 132 Country-Name Language Maps — Country and phone-code pickers read your app locale; state/city names remain as supplied by the dataset
  • Rich Country Data — 15+ fields per country including capitals, currencies, languages, timezones, borders
  • 40+ Utility Methods — Programmatic access to country data, search, statistics, and validation
  • Haptic Feedback — Tactile response on country selection
  • Smooth Animations — Fade transitions with configurable duration
  • Full Null Safety — Sound null safety throughout
  • Custom Icons — Ships with its own icon font (CountrifyIcons) — no Material Icons dependency for picker UI
  • Shared Building BlocksCountryFlag, CountryListTile, CountrySearchBar, CountryListView available as standalone widgets
  • Accessibility — Flag descriptions, country-row semantics, and tooltips on picker controls
  • focusedFillColor — Separate fill color when field has focus via CountrifyFieldStyle
  • focusedBoxShadow — Box shadow when field has focus via CountrifyFieldStyle
  • CitySearchField — Global city search with auto-state resolution — search all cities for a country without pre-selecting a state

Getting Started #

Requires Dart 3.6+ and Flutter 3.27+.

Installation #

Install countrify_light from pub.flutter-io.cn:

flutter pub add countrify_light

Or add it manually to your pubspec.yaml:

dependencies:
  countrify_light: ^1.0.0

After adding the dependency manually, run:

flutter pub get

Import #

import 'package:countrify_light/countrify_light.dart';

Quick Start #

Replace your app's lib/main.dart with this runnable country-picker example:

import 'package:countrify_light/countrify_light.dart';
import 'package:flutter/material.dart';

void main() => runApp(const CountryPickerApp());

class CountryPickerApp extends StatelessWidget {
  const CountryPickerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Choose a country')),
        body: Padding(
          padding: const EdgeInsets.all(16),
          child: CountryDropdownField(
            initialCountryCode: CountryCode.us,
            showPhoneCode: false,
            onChanged: (country) => debugPrint(country.alpha2Code),
          ),
        ),
      ),
    );
  }
}

The package declares its own assets and icon font; you do not need to copy them or add them to your app's asset list. Country data is available synchronously. State and city assets load asynchronously when needed.

The remaining widget snippets are fragments for your widget tree. Examples using setState assume a StatefulWidget with the shown selection variables. Import package:flutter/services.dart when using input formatters.

Example App #

The example app demonstrates phone input, country selection, cascading addresses, themes, localization, and shared widgets. From the repository root:

cd example
flutter pub get
flutter run

Display Modes #

PhoneNumberField, CountryDropdownField, PhoneCodePicker, StatePicker, and CityPicker use pickerMode for bottom sheet, dialog, full screen, or dropdown presentation. CountryPickerMode.none disables picker opening. For an embedded country list, use CountryPicker(pickerType: CountryPickerType.inline); CountryPickerMode has no inline value.

StateDropdownField and CityDropdownField default to searchable text fields with suggestion overlays. Set searchable: false to use their pickerMode instead. CitySearchField always uses a search overlay.

1. Bottom Sheet #

A modal bottom sheet that slides up from the bottom of the screen. Best for mobile-first UIs.

PhoneNumberField(
  pickerMode: CountryPickerMode.bottomSheet,
  onChanged: (phoneNumber, country) { },
)

// Or with CountryDropdownField:
CountryDropdownField(
  pickerMode: CountryPickerMode.bottomSheet,
  onChanged: (country) { },
)

2. Dialog #

A centered dialog popup. Best for tablet and desktop layouts.

CountryDropdownField(
  pickerMode: CountryPickerMode.dialog,
  onChanged: (country) { },
)

3. Full Screen #

A full-screen page with an AppBar. Best for complex selection flows.

CountryDropdownField(
  pickerMode: CountryPickerMode.fullScreen,
  onChanged: (country) { },
)

4. Dropdown #

A compact scrollable dropdown anchored below the field. Best for forms.

PhoneNumberField(
  pickerMode: CountryPickerMode.dropdown,
  onChanged: (phoneNumber, country) { },
)

5. Inline #

Use CountryPicker directly in your layout for an inline embedded list. Best for dashboard or settings pages.

CountryPicker(
  pickerType: CountryPickerType.inline,
  onCountrySelected: (country) {
    setState(() => selectedCountry = country);
  },
  showPhoneCode: true,
  searchEnabled: true,
)

Widgets #

CountryPicker #

The primary widget for country selection. Embed it directly in your layout or use it as the foundation for custom picker UIs. Supports rich theming, filtering, sorting, and all display options.

CountryPicker(
  initialCountryCode: CountryCode.us,
  onCountrySelected: (country) {
    print('Selected: ${country.name}');
  },
  theme: CountryPickerTheme.darkTheme(),
  config: const CountryPickerConfig(),
  showPhoneCode: true,
  showFlag: true,
  showCountryName: true,
  showCapital: false,
  showRegion: false,
  showPopulation: false,
  searchEnabled: true,
  filterEnabled: false,
)

PhoneNumberField #

A phone text field with an integrated calling-code picker as a prefix. Tapping the prefix opens a dropdown by default, or a bottom sheet, dialog, or full-screen picker when configured. The callback returns the entered text and selected country; it does not validate or normalize an international phone number. Supply your own validator and formatters.

PhoneNumberField(
  initialCountryCode: CountryCode.us,
  style: const CountrifyFieldStyle(
    hintText: 'Enter phone number',
    labelText: 'Phone',
  ),
  onChanged: (phoneNumber, country) {
    print('Full number: +${country.callingCodes.first}$phoneNumber');
  },
  onCountryChanged: (country) {
    print('Country changed to: ${country.name}');
  },
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
    LengthLimitingTextInputFormatter(15),
  ],
  theme: CountryPickerTheme.defaultTheme(),
)

Customized PhoneNumberField:

PhoneNumberField(
  showDropdownIcon: true,
  flagSize: const Size(28, 20),
  dropdownMaxHeight: 300,
  pickerMode: CountryPickerMode.dropdown,  // dropdown, bottomSheet, dialog, fullScreen
  style: CountrifyFieldStyle.defaultStyle().copyWith(
    hintText: 'Phone number',
    focusedFillColor: Colors.blue.shade50,
    fieldBorderRadius: BorderRadius.circular(16),
    dialCodeTextStyle: const TextStyle(
      fontSize: 15,
      fontWeight: FontWeight.w700,
      color: Colors.blue,
    ),
  ),
  maxLength: 12,
  validator: (value) {
    if (value == null || value.isEmpty) return 'Phone number is required';
    return null;
  },
  onChanged: (phoneNumber, country) { },
)

Key Properties:

Property Type Default Description
initialCountryCode CountryCode? First country with calling code Pre-selected country code
controller TextEditingController? Internal Phone text controller
onChanged Function(String, Country)? Called on text or country change
onCountryChanged ValueChanged<Country>? Called when country changes
style CountrifyFieldStyle? null Unified style for decoration, text styles, cursor, divider, and prefix spacing
showFlag bool true Show flag in prefix
showDialCode bool true Show dial code in prefix
showDropdownIcon bool true Show dropdown arrow
pickerMode CountryPickerMode .dropdown How the picker opens (.none disables selection)
dropdownMaxHeight double 350 Max height of dropdown overlay
flagSize Size Size(24, 18) Flag dimensions
validator String? Function(String?)? Form validation
inputFormatters List<TextInputFormatter>? Input formatters
maxLength int? Maximum input length; use a digits-only formatter to restrict characters

style.toInputDecoration(...) preserves built-in country prefix UI by default and only overrides it when you explicitly set prefixIcon/suffixIcon.


CountryDropdownField #

A form-friendly widget that looks and behaves like a TextFormField. Tapping it opens a country picker. Ideal for registration forms and settings pages.

CountryDropdownField(
  initialCountryCode: CountryCode.us,
  onChanged: (country) {
    setState(() => selectedCountry = country);
  },
  style: CountrifyFieldStyle.defaultStyle().copyWith(
    hintText: 'Select a country',
  ),
  showPhoneCode: false,
  showFlag: true,
  searchEnabled: true,
  pickerMode: CountryPickerMode.bottomSheet, // or .dialog, .fullScreen, .dropdown, .none
  theme: CountryPickerTheme.defaultTheme(),
)

Key Properties:

Property Type Default Description
initialCountryCode CountryCode? null Pre-selected country code
onChanged ValueChanged<Country>? Selection callback
style CountrifyFieldStyle? null Unified style object for label/hint/borders/fill/text styles
showPhoneCode bool true Show calling code in display
showFlag bool true Show flag in prefix
showDropdownIcon bool true Show the built-in trailing caret; a style suffix icon takes precedence
pickerMode CountryPickerMode .bottomSheet How the picker opens (.none disables selection)
enabled bool true Whether the field is interactive
searchEnabled bool true Enables search in the picker
filterEnabled bool false Enables filter chips in the picker
customCountryBuilder CountryDropdownItemBuilder? null Custom country row forwarded to the picker
customHeaderBuilder Widget Function(BuildContext)? null Custom picker header
customSearchBuilder callback null Custom search field
customFilterBuilder callback null Custom filter controls

Use style: CountrifyFieldStyle.defaultStyle().copyWith(...) to customize field decoration and text styles.


PhoneCodePicker #

A widget for selecting a calling code, with bottom sheet, dialog, full-screen, and dropdown presentation. Use CountryPickerMode.none to disable opening.

PhoneCodePicker(
  initialCountryCode: CountryCode.us,
  onChanged: (country) {
    setState(() => selectedCountry = country);
  },
  showFlag: true,
  showCountryName: true,
  showDialCode: true,
  flagShape: FlagShape.circular,
  searchEnabled: true,
  pickerMode: CountryPickerMode.bottomSheet,
)

CountryStateCityField #

A composite cascading form widget that captures a complete country → state → city selection with three stacked dropdowns. Country data loads eagerly, states and cities are loaded lazily on demand from bundled assets, and selecting a parent clears the children.

CountryStateCityField(
  initialCountryCode: CountryCode.us,
  onChanged: (selection) {
    print(selection.country?.name);   // e.g. "United States"
    print(selection.state?.name);     // e.g. "California"
    print(selection.city?.name);      // e.g. "San Francisco"
  },
  fieldStyle: CountrifyFieldStyle.defaultStyle(),
  searchEnabled: true,
)

The dataset ships with 250 country/territory records (249 officially assigned ISO 3166-1 entries plus XK/Kosovo), 5,308 states / provinces, and 152,970 cities — sourced from dr5hn/countries-states-cities-database and split into per-country / per-state JSON files under assets/geo/ so only the records for the currently selected country / state are decoded. The bundled geo payload omits coordinates; the nullable coordinate fields remain available for custom AssetBundle data sources.

For programmatic access, use GeoRepository:

final repo = GeoRepository.instance;
final states = await repo.statesOf('PK');     // List<CountryState>
if (states.isNotEmpty) {
  final cities = await repo.citiesOf(states.first.id); // List<City>
  debugPrint('${cities.length} cities');
}

StatePicker #

Standalone picker for the states / provinces / regions of a single country. Supports four display modes (bottomSheet, dialog, fullScreen, dropdown) and is fully themeable.

StatePicker(
  countryIso2: 'US',
  initialStateId: 1416,
  pickerMode: CountryPickerMode.bottomSheet,
  sortBy: StateSortBy.name,          // or .type, .id
  theme: GeoPickerTheme.light(),
  config: const GeoPickerConfig(
    title: 'Pick your state',
    searchHintText: 'Search states',
    hapticFeedback: true,
  ),
  customStateBuilder: (ctx, state, selected) => Row(
    children: [
      Expanded(child: Text(state.name)),
      if (state.iso2 != null) Text(state.iso2!),
    ],
  ),
  onStateSelected: (state) => print(state.name),
)

StatePicker and CityPicker support:

  • Display modes via pickerMode — bottom sheet, dialog, full screen, dropdown, or none to suppress opening
  • Sort order via sortBy
  • Accent-insensitive searchsao paulo matches São Paulo (toggle via GeoPickerConfig.accentInsensitiveSearch)
  • Debounced search with configurable delay, initial query, and autofocus
  • Live clear button that reacts instantly to typing
  • onSearchChanged(query) and onResultsChanged(List<T>) callbacks for observing search state
  • customMatcher hook for fully custom matching logic (fuzzy search, ISO-only search, etc.)
  • Theme (GeoPickerTheme) — item, header, search, and overlay styling with light/dark presets
  • Config (GeoPickerConfig) — behavior, haptics, heights, text labels
  • Custom row builder (customStateBuilder)
  • Custom header / search / empty-state builders for full control

Search example

StatePicker(
  countryIso2: 'BR',
  config: const GeoPickerConfig(
    initialSearchText: 'sao',            // Pre-fills the search field
    searchDebounce: Duration(milliseconds: 200),
    accentInsensitiveSearch: true,       // "sao paulo" matches "São Paulo"
    autofocusSearch: true,
  ),
  onSearchChanged: (q) => print('typed: $q'),
  onResultsChanged: (results) => print('${results.length} match'),
  onStateSelected: (s) => print(s.name),
)

A customMatcher replaces the default matching logic. If you provide one, normalize your candidate text with SearchNormalizer.foldAccents to retain accent-insensitive matching.

Or build a custom search field using customSearchBuilder:

StatePicker(
  countryIso2: 'US',
  customSearchBuilder: (context, controller) => TextField(
    controller: controller,               // Wiring the provided controller is required
    decoration: InputDecoration(
      prefixIcon: const Icon(Icons.travel_explore),
      labelText: 'Find your state',
      border: OutlineInputBorder(borderRadius: BorderRadius.circular(30)),
    ),
  ),
  onStateSelected: (s) => print(s.name),
)

CityPicker #

Mirrors StatePicker but takes a stateId instead of countryIso2.

CityPicker(
  stateId: 1416, // California in the bundled snapshot
  initialCityId: 125809, // San Francisco in the bundled snapshot
  pickerMode: CountryPickerMode.dialog,
  sortBy: CitySortBy.name,
  theme: GeoPickerTheme.dark(),
  onCitySelected: (city) => print(city.name),
)

IDs come from the bundled dataset; look them up through GeoRepository when integrating your own stored selections.

The bundled geo data omits coordinates. showCoordinates is only useful with a custom repository or AssetBundle that supplies latitude and longitude; bundled records return null for those fields.


StateDropdownField #

A searchable state field by default. Set searchable: false for a tap-to-open StatePicker, as shown below. CountrifyFieldStyle controls its field decoration.

String? _countryIso2 = 'US';
CountryState? _state;

StateDropdownField(
  countryIso2: _countryIso2,                // null → disabled
  searchable: false,                      // Use pickerMode instead of suggestions
  initialStateId: _state?.id,
  style: CountrifyFieldStyle.defaultStyle().copyWith(labelText: 'State'),
  pickerTheme: GeoPickerTheme.light(),
  pickerConfig: const GeoPickerConfig(searchEnabled: true),
  pickerMode: CountryPickerMode.bottomSheet,
  sortBy: StateSortBy.name,
  showType: true,                            // "province" / "region" as subtitle
  onChanged: (s) => setState(() => _state = s),
)

Changing countryIso2 automatically clears the selection and refetches states. The field shows an inline spinner while loading.

Pre-filling in edit mode — pass initialStateName to pre-fill from a backend string without needing a state ID:

StateDropdownField(
  countryIso2: 'US',
  initialStateName: 'California', // Use your saved state name
  onChanged: (s) => setState(() => _state = s),
)

CityDropdownField #

Companion of StateDropdownField; changing stateId clears the city. Like the state field, it defaults to searchable suggestions. Here _city is a City? stored in your widget state.

CityDropdownField(
  stateId: _state?.id,
  initialCityId: _city?.id,
  searchable: false, // Omit to keep the default searchable field
  style: CountrifyFieldStyle.outlineStyle(),
  pickerMode: CountryPickerMode.bottomSheet,
  onChanged: (c) => setState(() => _city = c),
)

CitySearchField #

Searchable text field that searches across all cities for a country without requiring a pre-selected state. When a city is selected the parent state is resolved automatically.

CitySearchField(
  countryIso2: 'US',
  style: CountrifyFieldStyle.defaultStyle().copyWith(
    focusedBoxShadow: [
      BoxShadow(
        color: Colors.blue.withValues(alpha: 0.15),
        blurRadius: 8,
        spreadRadius: 2,
      ),
    ],
  ),
  onChanged: (result) {
    if (result != null) {
      print('${result.city.name}, ${result.state.name}');
      // e.g. "San Francisco, California"
    }
  },
)

After selection the field shows the city name and the onChanged callback provides a CitySearchResult record containing both the City and its parent CountryState. City files are pre-loaded in the background on init for snappy search. Changing countryIso2 clears the selection and re-preloads.

Pre-filling in edit mode — pass initialCityName to pre-fill from a backend string without needing a city ID:

CitySearchField(
  countryIso2: 'US',
  initialCityName: 'San Francisco', // Use your saved city name
  onChanged: (result) => debugPrint(result?.city.name),
)

Shared Building Blocks #

Countrify Light exports these shared widgets so you can compose custom UIs:

CountryFlag — Displays a country's platform flag emoji:

CountryFlag(
  country: CountryUtils.getCountryByAlpha2Code('US')!,
  size: const Size(32, 24),
  borderRadius: BorderRadius.circular(4),
)

CountryListTile — A ready-made list tile showing flag, name, and optional phone code:

CountryListTile(
  country: country,
  showDialCode: true,
  onTap: (selected) => debugPrint(selected.name),
)

CountrySearchBar — A themed, debounced search input. Its callback provides the query; connect it to your own filtering logic:

CountrySearchBar(
  onChanged: (query) => print('Searching: $query'),
  theme: CountryPickerTheme.defaultTheme(),
)

Theming #

CountryPickerTheme provides light, dark, and Material 3 presets, plus custom() to build a theme from your colors. Use GeoPickerTheme for state/city pickers and CountrifyFieldStyle for field decoration.

Built-in Themes #

// Default light theme
CountryPickerTheme.defaultTheme()

// Dark theme
CountryPickerTheme.darkTheme()

// Material Design 3 theme
CountryPickerTheme.material3Theme()

// Custom theme from a primary color
CountryPickerTheme.custom(
  primaryColor: Colors.teal,
  backgroundColor: Colors.white,
  isDark: false,
)

Applying a Theme #

CountryPicker(
  onCountrySelected: (country) { },
  theme: CountryPickerTheme.darkTheme(),
)

Custom Themes #

Use CountryPickerTheme.custom() for quick theming, or copyWith() for fine-grained control:

final customTheme = CountryPickerTheme.custom(
  primaryColor: Colors.deepPurple,
  backgroundColor: Colors.white,
  isDark: false,
).copyWith(
  countryItemBorderRadius: BorderRadius.circular(16),
  searchBarBorderRadius: BorderRadius.circular(24),
  headerTextStyle: const TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    letterSpacing: 1.2,
  ),
);

Full Theme Properties #

Common CountryPickerTheme properties:

const theme = CountryPickerTheme(
  // ─── Background ──────────────────────────────────
  backgroundColor: Colors.white,
  headerColor: Color(0xFFF5F5F5),

  // ─── Header ──────────────────────────────────────
  headerTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
  headerIconColor: Colors.black54,

  // ─── Search Bar ──────────────────────────────────
  searchBarColor: Color(0xFFF8F9FA),
  searchTextStyle: TextStyle(fontSize: 16),
  searchHintStyle: TextStyle(fontSize: 16, color: Colors.black54),
  searchIconColor: Colors.black54,
  searchBarBorderColor: Color(0xFFE0E0E0),
  searchBarBorderRadius: BorderRadius.all(Radius.circular(12)),
  searchHintText: 'Search countries...',
  searchCursorColor: Colors.blue,
  searchFocusedBorderColor: Colors.blue,
  searchInputDecoration: null,         // Full InputDecoration override

  // ─── Country Items ───────────────────────────────
  countryItemBackgroundColor: Colors.white,
  countryItemSelectedColor: Color(0xFFE3F2FD),
  countryItemSelectedBorderColor: Color(0xFF2196F3),
  countryItemSelectedIconColor: Color(0xFF2196F3),
  countryItemBorderRadius: BorderRadius.all(Radius.circular(8)),
  countryNameTextStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
  countrySubtitleTextStyle: TextStyle(fontSize: 14, color: Colors.grey),
  compactCountryNameTextStyle: TextStyle(fontSize: 13, color: Colors.black54),
  compactDialCodeTextStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
  readOnlyHintTextStyle: TextStyle(fontSize: 14, color: Colors.black54),
  flagEmojiTextStyle: TextStyle(fontSize: 16),
  appBarTitleTextStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
  dialogOptionTextStyle: TextStyle(fontSize: 14),
  dialogActionTextStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),

  // ─── Filter Chips ────────────────────────────────
  filterBackgroundColor: Color(0xFFF0F0F0),
  filterSelectedColor: Color(0xFF2196F3),
  filterTextColor: Colors.black87,
  filterSelectedTextColor: Colors.white,
  filterCheckmarkColor: Colors.white,
  filterIconColor: Colors.black54,

  // ─── Borders & Elevation ─────────────────────────
  borderColor: Color(0xFFE0E0E0),
  borderRadius: BorderRadius.all(Radius.circular(20)),
  elevation: 8.0,
  shadowColor: Color(0x1A000000),

  // ─── Scrollbar ───────────────────────────────────
  scrollbarThickness: 6.0,
  scrollbarRadius: BorderRadius.all(Radius.circular(3)),

  // ─── Dropdown-Specific ───────────────────────────
  dropdownMenuBackgroundColor: Colors.white,
  dropdownMenuElevation: 8,
  dropdownMenuBorderRadius: BorderRadius.all(Radius.circular(12)),
  dropdownMenuBorderColor: Colors.grey,
  dropdownMenuBorderWidth: 1,

  // ─── Customizable Icons ──────────────────────────
  closeIcon: CountrifyIcons.x,
  searchIcon: CountrifyIcons.search,
  clearIcon: CountrifyIcons.circleX,
  selectedIcon: CountrifyIcons.circleCheckBig,
  filterIcon: CountrifyIcons.listFilter,
  dropdownIcon: CountrifyIcons.chevronDown,
  emptyStateIcon: CountrifyIcons.searchX,
  defaultCountryIcon: CountrifyIcons.globe,

  // ─── Behavior ────────────────────────────────────
  animationDuration: Duration(milliseconds: 300),
  hapticFeedback: true,
);

Configuration #

CountryPickerConfig contains shared options used by country and phone-code widgets.

Use widget-level parameters on CountryPicker for advanced behavior (custom builders, sorting/filter defaults, advanced flag shape/size/shadow, sizing, etc.).

Display Options #

const config = CountryPickerConfig(
  locale: 'en',                      // Optional locale override
  enableSearch: true,                // Search toggle for PhoneNumberField
  includeRegions: ['Europe'],        // Shared include filter
  includeCountries: ['DE', 'FR'],    // Must also match includeRegions
  excludeCountries: ['AQ'],          // Shared exclude by alpha-2
);

Flag Customization #

Shared flag styling via config (border only):

const config = CountryPickerConfig(
  flagBorderRadius: BorderRadius.all(Radius.circular(6)),
  flagBorderColor: Colors.grey,
  flagBorderWidth: 2,
);

Advanced flag styling via CountryPicker (widget-level):

CountryPicker(
  onCountrySelected: (country) { },
  flagShape: FlagShape.rounded,
  flagSize: Size(40, 28),
  flagShadowColor: Colors.black26,
  flagShadowBlur: 6,
  flagShadowOffset: Offset(0, 3),
)

Filtering Countries #

const config = CountryPickerConfig(
  // Include only specific regions
  includeRegions: ['Europe', 'Asia'],

  // Include only specific countries (by alpha-2 code)
  includeCountries: ['DE', 'FR', 'JP'],

  // Exclude specific countries (by alpha-2 code)
  excludeCountries: ['JP'],
);

Include filters are combined: a country must match both includeRegions and includeCountries when both are supplied. excludeCountries then removes matches. The example above keeps Germany and France. On CountryPicker, use excludeRegions when no region inclusion filter is set; a non-empty region inclusion filter takes precedence.

Use the widget's searchEnabled parameter on CountryPicker, CountryDropdownField, and PhoneCodePicker. PhoneNumberField exposes its search toggle through CountryPickerConfig.enableSearch.

Sorting #

Sorting is a widget-level parameter on CountryPicker:

CountryPicker(
  onCountrySelected: (country) { },
  sortBy: CountrySortBy.name,        // Alphabetical (default)
  // sortBy: CountrySortBy.population, // Most populous first
  // sortBy: CountrySortBy.area,       // Largest area first
  // sortBy: CountrySortBy.region,     // Grouped by region
  // sortBy: CountrySortBy.capital,    // Alphabetical by capital
)

Sizing #

Sizing is a widget-level parameter on CountryPicker:

CountryPicker(
  onCountrySelected: (country) { },
  maxHeight: 600.0,           // Maximum picker height
  minHeight: 200.0,           // Minimum picker height
  dropdownMaxHeight: 400.0,   // Maximum dropdown menu height
)

Custom Builders #

Custom builders are available on CountryPicker (not shared config):

CountryPicker(
  onCountrySelected: (country) { },
  // Custom country item
  customCountryBuilder: (context, country, isSelected) {
    return ListTile(
      leading: CountryFlag(
        country: country,
        size: const Size(32, 24),
      ),
      title: Text(country.name),
      subtitle: country.callingCodes.isEmpty
          ? null
          : Text('+${country.callingCodes.first}'),
      trailing: isSelected
          ? const Icon(Icons.check_circle, color: Colors.blue)
          : null,
    );
  },

  // Custom header
  customHeaderBuilder: (context) {
    return const Padding(
      padding: EdgeInsets.all(16),
      child: Text('Pick Your Country',
          style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
    );
  },

  // Custom search bar
  customSearchBuilder: (context, controller, onChanged) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: TextField(
        controller: controller,
        onChanged: onChanged,
        decoration: const InputDecoration(
          hintText: 'Type to search...',
          prefixIcon: Icon(Icons.search),
        ),
      ),
    );
  },

  // Custom filter bar
  customFilterBuilder: (context, filter, onChanged) {
    return Wrap(
      children: ['Europe', 'Asia', 'Africa'].map((region) {
        return FilterChip(
          label: Text(region),
          selected: filter.regions.contains(region),
          onSelected: (selected) {
            final regions = selected
                ? [...filter.regions, region]
                : filter.regions.where((r) => r != region).toList();
            onChanged(filter.copyWith(regions: regions));
          },
        );
      }).toList(),
    );
  },
)

Customizable Strings #

CountryPickerConfig controls shared strings used by multiple widgets:

const config = CountryPickerConfig(
  titleText: 'Choose Your Country',       // Shared picker title
  searchHintText: 'Type to search...',    // Shared search placeholder
  emptyStateText: 'Nothing found',        // Shared empty state message
  selectCountryHintText: 'Tap to choose', // Shared unselected hint
);

Filter labels are widget-level parameters on CountryPicker:

CountryPicker(
  onCountrySelected: (country) { },
  filterTitleText: 'Filter Options',       // Filter dialog title
  filterSortByText: 'Sort:',               // Filter sort label
  filterRegionsText: 'Regions:',           // Filter regions label
  filterAllText: 'All',                    // "All" filter chip label
  filterCancelText: 'Cancel',              // Filter cancel button
  filterApplyText: 'Done',                 // Filter apply button
)

Shared strings default to English and can be configured independently of country-name translations.

Parameter Default Where it appears
titleText 'Select Country' Country picker header
searchHintText 'Search countries...' Search bar placeholder
emptyStateText 'No countries found' Empty search results
selectCountryHintText 'Select a country' Dropdown/field placeholder
filter*Text Widget-level params Comprehensive filter UI

Country Data & Utilities #

Use CountryUtils to access country data programmatically without showing any picker UI.

Fetching Countries #

// Get all 250 country/territory records (249 ISO 3166-1 + XK/Kosovo)
final countries = CountryUtils.getAllCountries();

// Get by ISO code
final usa = CountryUtils.getCountryByAlpha2Code('US');
final canada = CountryUtils.getCountryByAlpha3Code('CAN');
final germany = CountryUtils.getCountryByNumericCode('276');

// Search by name (case-insensitive)
final results = CountryUtils.searchCountries('united');

// Get by region or subregion
final european = CountryUtils.getCountriesByRegion('Europe');
final southAmerican = CountryUtils.getCountriesBySubregion('South America');

// Get by calling code
final countriesWith1 = CountryUtils.getCountriesByCallingCode('1'); // No '+' prefix

// Get by currency
final euroCountries = CountryUtils.getCountriesByCurrencyCode('EUR');

// Get by language
final englishSpeaking = CountryUtils.getCountriesByLanguageCode('en');

// Get bordering countries
final neighbors = CountryUtils.getBorderCountries('USA');

Sorting #

final byPopulation = CountryUtils.getCountriesSortedByPopulation();
final byArea = CountryUtils.getCountriesSortedByArea();
final alphabetical = CountryUtils.getCountriesSortedByName();

Filtered Collections #

final independent = CountryUtils.getIndependentCountries();
final unMembers = CountryUtils.getUnMemberCountries();

Statistics #

final totalPopulation = CountryUtils.getTotalWorldPopulation();
final totalArea = CountryUtils.getTotalWorldArea();

final mostPopulous = CountryUtils.getMostPopulousCountry();
final largest = CountryUtils.getLargestCountry();
final smallest = CountryUtils.getSmallestCountry();

// Formatted output
print(CountryUtils.formatPopulation(totalPopulation));
print(CountryUtils.formatArea(totalArea)); // Rounded to whole km²

// Deterministic formatting examples:
print(CountryUtils.formatPopulation(1234567)); // '1,234,567'
print(CountryUtils.formatArea(1234.56));       // '1,235'

These totals describe the bundled snapshot. Population values are not live estimates, and summing country/territory records is not an authoritative world population or land-area measurement.

Metadata Lookups #

final regions = CountryUtils.getAllRegions();         // ["Africa", "Americas", ...]
final subregions = CountryUtils.getAllSubregions();   // ["Caribbean", "Central Asia", ...]
final currencies = CountryUtils.getAllCurrencies();
final languages = CountryUtils.getAllLanguages();
final timezones = CountryUtils.getAllTimezones();

Validation #

CountryUtils.isValidAlpha2Code('US');   // true
CountryUtils.isValidAlpha3Code('USA');  // true
CountryUtils.isValidNumericCode('840'); // true
CountryUtils.isValidAlpha2Code('XX');   // false

Localization (132 Languages) #

Countrify Light ships with built-in country-name translations for 132 languages, sourced from CLDR data. All translations are compile-time constants — no third-party runtime packages, no network requests, and no runtime JSON parsing.

CountryPicker, CountryDropdownField, PhoneCodePicker, and PhoneNumberField use your app locale for country names. Country-name translations do not translate state/city names, field labels, search placeholders, or filter labels; configure those UI strings separately. The 132 bundled country-name maps are separate from the set of locales supported by Flutter's Material localization delegates.

// 1. Add flutter_localizations to your pubspec.yaml:
//    dependencies:
//      flutter_localizations:
//        sdk: flutter

// 2. Configure your MaterialApp:
import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
  locale: const Locale('ja'), // A language supported by your app's delegates
  supportedLocales: const [
    Locale('ja'),
    Locale('en'),
    // ... your supported locales
  ],
  localizationsDelegates: const [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  // ...
);

// 3. Place this field inside the MaterialApp widget tree.
PhoneNumberField(
  onChanged: (phoneNumber, country) { },
)
// → shows アメリカ合衆国, ドイツ, フランス, ...

Country and phone-code widgets use the locale's languageCode. For an explicit override, pass a supported language code such as de or ja, rather than a region-tagged value such as de-DE. Unsupported country-name translations fall back to the English country name.

Explicit Locale Override #

Use CountryPickerConfig.locale to override the auto-detected locale for a specific widget:

// Force German names on this picker, regardless of app locale
CountryPicker(
  onCountrySelected: (country) { },
  config: CountryPickerConfig(locale: 'de'),
)

// Force English even if app locale is non-English
PhoneNumberField(
  onChanged: (phoneNumber, country) { },
  config: CountryPickerConfig(locale: 'en'),
)
Scenario What to do
App already has a locale set Nothing — auto-detected
Override locale for one widget CountryPickerConfig(locale: 'de')
Force English in a non-English app CountryPickerConfig(locale: 'en')
No locale set anywhere Defaults to English

Programmatic Access #

Use CountryUtils to get localized names in code (outside of widgets):

final usa = CountryUtils.getCountryByAlpha2Code('US')!;

CountryUtils.getCountryNameInLanguage(usa, 'de'); // "Vereinigte Staaten"
CountryUtils.getCountryNameInLanguage(usa, 'fr'); // "États-Unis"
CountryUtils.getCountryNameInLanguage(usa, 'ja'); // "アメリカ合衆国"
CountryUtils.getCountryNameInLanguage(usa, 'ar'); // "الولايات المتحدة"
CountryUtils.getCountryNameInLanguage(usa, 'zh'); // "美国"
CountryUtils.getCountryNameInLanguage(usa, 'hi'); // "संयुक्त राज्य"

The method checks the country's nameTranslations map first (for user-provided overrides), then falls back to the built-in CLDR data, and finally returns the English name.

Get All Translations for a Country #

final allNames = CountryUtils.getCountryNamesInAllLanguages(usa);
// Returns available translations as a Map<String, String>:
// {"af": "Verenigde State van Amerika", "ar": "الولايات المتحدة", "de": "Vereinigte Staaten", ...}

List Supported Locales #

final locales = CountryUtils.getSupportedLocales();
// ["af", "ak", "am", "ar", "as", "az", "be", "bg", ..., "zh", "zu"]
// 132 locale codes

Direct Access via CountryNameL10n #

For lower-level access without going through CountryUtils:

import 'package:countrify_light/countrify_light.dart';

// Get a single translation
final name = CountryNameL10n.getLocalizedName('DE', 'fr'); // "Allemagne"

// Get all country names for a locale
final frenchNames = CountryNameL10n.getTranslationsForLocale('fr');
// {"AD": "Andorre", "AE": "Émirats arabes unis", "AF": "Afghanistan", ...}

// Check supported locales
final locales = CountryNameL10n.supportedLocales; // 132 entries

Supported Languages #

Full list of 132 supported language codes

af ak am ar as az be bg bm bn bo br bs ca ce cs cy da de dz ee el en eo es et eu fa ff fi fo fr fy ga gd gl gu gv ha he hi hr hu hy ia id ig ii is it ja jv ka ki kk kl km kn ko ks ku kw ky lb lg ln lo lt lu lv mg mi mk ml mn mr ms mt my nb nd ne nl nn no om or os pa pl ps pt qu rm rn ro ru rw se sg si sk sl sn so sq sr sv sw ta te tg th ti tk tl to tr tt ug uk ur uz vi vo wo xh yo zh zu


Country Model #

The main model fields are listed below (declarations only; constructors and methods are omitted). Field comments illustrate the data shape, not live demographic values:

class Country {
  final String name;                          // "United States"
  final Map<String, String> nameTranslations; // Canonical English compatibility map
  final String alpha2Code;                    // "US"
  final String alpha3Code;                    // "USA"
  final String numericCode;                   // "840"
  final String flagEmoji;                     // Unicode flag emoji
  final String flagImagePath;                 // Empty legacy compatibility field
  final String capital;                       // "Washington, D.C."
  final String? largestCity;                  // Null in the bundled catalogue
  final String region;                        // "Americas"
  final String subregion;                     // "Northern America"
  final int population;                       // Source snapshot; 0 if missing
  final double area;                          // Source area in km²
  final List<String> callingCodes;            // ["1"]
  final List<String> topLevelDomains;         // [".us"]
  final List<Currency> currencies;            // [Currency(code: "USD", ...)]
  final List<Language> languages;             // [Language(name: "English", ...)]
  final List<String> timezones;               // ["America/New_York", ...]
  final List<String> borders;                 // ["CAN", "MEX"]
  final bool isIndependent;                   // true
  final bool isUnMember;                      // true
  final PhoneMetadata? phoneMetadata;         // Null in the bundled catalogue
}

class Currency {
  final String code;    // "USD"
  final String name;    // "United States dollar"
  final String symbol;  // "$"
}

class Language {
  final String iso6391;    // "en", or empty when unavailable
  final String iso6392;    // Legacy field containing ISO 639-3, e.g. "eng"
  final String name;       // English source name
  final String nativeName; // Native name when available; otherwise English
}

Localized country display names come from CountryNameL10n; they are not duplicated into every generated Country.nameTranslations map. The bundled sources also do not provide reliable largest-city or phone-validation metadata, so those optional fields remain null instead of containing guessed values.


Enums Reference #

FlagShape #

Value Description
FlagShape.rectangular Standard rectangular flag (default)
FlagShape.circular Circular cropped flag
FlagShape.rounded Rounded rectangle flag

CountrySortBy #

Value Description
CountrySortBy.name Alphabetical by country name (default)
CountrySortBy.population Descending by population
CountrySortBy.area Descending by area
CountrySortBy.region Alphabetical by region
CountrySortBy.capital Alphabetical by capital city

CountryPickerType #

Used by CountryPicker through pickerType. This selects the widget layout; embedding it does not push a route or open a modal. For a field that opens a picker when tapped, use CountryDropdownField.

Value Description
CountryPickerType.bottomSheet Sheet-style list layout (default)
CountryPickerType.dialog Dialog-style layout
CountryPickerType.fullScreen Layout with an app bar
CountryPickerType.dropdown Trigger with an anchored dropdown
CountryPickerType.inline Embedded inline list
CountryPickerType.none Read-only mode (disables changing selection)

CountryPickerMode #

Used by country/phone-code fields and state/city pickers to control presentation. For StateDropdownField and CityDropdownField, set searchable: false to use these modes.

Value Description
CountryPickerMode.dropdown Compact scrollable dropdown anchored below the field
CountryPickerMode.bottomSheet Modal bottom sheet
CountryPickerMode.dialog Centered dialog popup
CountryPickerMode.fullScreen Full screen page
CountryPickerMode.none Read-only mode (disables changing selection)

Real-World Examples #

Phone Number Input with PhoneNumberField #

PhoneNumberField(
  style: const CountrifyFieldStyle(
    hintText: 'Enter phone number',
    labelText: 'Phone',
  ),
  theme: CountryPickerTheme.defaultTheme(),
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
    LengthLimitingTextInputFormatter(15),
  ],
  onChanged: (phoneNumber, country) {
    print('Full number: +${country.callingCodes.first}$phoneNumber');
  },
  onCountryChanged: (country) {
    print('Country changed: ${country.name}');
  },
)

Registration Form with CountryDropdownField #

CountryDropdownField(
  initialCountryCode: _selectedCountryCode,
  onChanged: (country) {
    setState(() => _selectedCountry = country);
  },
  style: CountrifyFieldStyle.defaultStyle().copyWith(
    hintText: 'Select your country',
  ),
  showPhoneCode: false,
  showFlag: true,
  searchEnabled: true,
  pickerMode: CountryPickerMode.bottomSheet,
)

European Countries Only #

CountryPicker(
  onCountrySelected: (country) { },
  config: const CountryPickerConfig(
    includeRegions: ['Europe'],
  ),
  showPhoneCode: true,
  searchEnabled: true,
)

Dark Theme Picker #

CountryPicker(
  onCountrySelected: (country) { },
  theme: CountryPickerTheme.darkTheme(),
  showPhoneCode: true,
  searchEnabled: true,
)

Circular Flags #

CountryPicker(
  onCountrySelected: (country) { },
  flagShape: FlagShape.circular,
  flagSize: const Size(40, 40),
  showPhoneCode: true,
)

Custom Country Item Builder #

CountryPicker(
  onCountrySelected: (country) { },
  customCountryBuilder: (context, country, isSelected) {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: isSelected ? Colors.blue.shade50 : Colors.white,
        border: Border.all(
          color: isSelected ? Colors.blue : Colors.grey.shade300,
        ),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Row(
        children: [
          CountryFlag(
            country: country,
            size: const Size(48, 36),
            borderRadius: BorderRadius.circular(4),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(country.name,
                    style: const TextStyle(fontWeight: FontWeight.w600)),
                if (country.callingCodes.isNotEmpty)
                  Text('+${country.callingCodes.first}',
                      style: TextStyle(color: Colors.grey.shade600)),
              ],
            ),
          ),
          if (isSelected) const Icon(Icons.check_circle, color: Colors.blue),
        ],
      ),
    );
  },
)

Using CountryFlag Standalone #

Use the CountryFlag widget to display a country's flag anywhere in your app:

CountryFlag(
  country: CountryUtils.getCountryByAlpha2Code('US')!,
  size: const Size(32, 24),
  borderRadius: BorderRadius.circular(4),
)

Troubleshooting #

Flag emoji not rendering as expected

Use the CountryFlag widget so sizing, clipping, and semantics stay consistent. Rendering follows the platform's emoji font.

CountryFlag(
  country: country,
  size: const Size(32, 24),
)
Picker not appearing

Ensure the widget is placed inside a valid widget tree with a BuildContext that has access to a Navigator. If using CountryDropdownField or PhoneNumberField, ensure they are inside a Scaffold or similar root widget.

Country not found by code

CountryUtils alpha-2 and alpha-3 lookups ignore case and surrounding whitespace. Unknown codes return null:

final country = CountryUtils.getCountryByAlpha2Code(' us '); // United States
final unknown = CountryUtils.getCountryByAlpha2Code('XX');   // null
Selected country not appearing at top of list

Pass the initialCountryCode parameter so the picker places it at the top:

CountryPicker(
  initialCountryCode: _selectedCountryCode,
  onCountrySelected: (country) { },
)
PhoneNumberField dropdown not dismissing

The dropdown overlay dismisses when tapping outside it. If you're embedding PhoneNumberField in a scrollable view, ensure the overlay has space to render below the field. You can adjust dropdownMaxHeight to control its size.


FAQ #

Is this package free to use?

Yes. The package code is available under the MIT License. Bundled geographic and country databases are separately licensed under ODbL 1.0, while bundled country-name translations retain their upstream MIT notice. See THIRD_PARTY_NOTICES.md.

Does it work on all platforms?

The package targets iOS, Android, Web, macOS, Windows, and Linux. Flag appearance depends on the platform emoji font; some environments display regional-indicator letters instead of a flag image.

How large is the package?

Countrify Light omits bundled PNG flags and geographic coordinates. Country data and language maps are compiled Dart constants; state/city JSON assets are bundled with the app and decoded on demand. Lazy loading reduces work at runtime, not the amount of asset data shipped. Measure the final build for your target platform to determine app-size impact.

Does it support RTL languages?

Yes. The package respects Flutter's text direction settings. Country name translations are available for RTL languages including Arabic (ar), Hebrew (he), Persian/Farsi (fa), Urdu (ur), and Pashto (ps).

How does localization work?

Country and phone-code widgets use your app locale for country names. CountryPickerConfig(locale: 'ja') overrides that choice. State/city names remain in the source dataset's form, and UI labels need your own translations. See Localization for setup and fallback behavior.

Can I customize the picker's UI text strings?

Yes. Shared strings (title/search/empty/hint) are configurable via CountryPickerConfig. Filter labels are configurable via CountryPicker widget parameters.

Can I filter countries?

Yes. Use shared include/exclude filters in CountryPickerConfig and sorting/filter defaults via CountryPicker widget parameters.

Can I provide my own country item UI?

Yes. Use customCountryBuilder (and related custom builders) on CountryPicker.

Is the country data accurate?

The country catalogue is generated from pinned dr5hn and mledoze revisions; the bundled state/city hierarchy is also derived from dr5hn. These are community-maintained datasets and can contain errors or lag geopolitical changes. See THIRD_PARTY_NOTICES.md for the exact sources and report inaccuracies via GitHub issues.

What's the difference between CountryDropdownField and PhoneNumberField?

CountryDropdownField is a form field for selecting a country (displays country name/flag). PhoneNumberField is a complete phone input widget that combines a country code picker prefix with a text input for the phone number.


Contributing #

Contributions are welcome! Here's how to get started:

# Clone the repository
git clone https://github.com/gokdeemir/countrify-light.git
cd countrify-light

# Install dependencies
flutter pub get

# Run package tests
flutter test

# Run analysis
flutter analyze

# Run the example app
cd example
flutter pub get
flutter run
  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Commit your changes (git commit -m 'Add my feature')
  4. Push to the branch (git push origin feature/my-feature)
  5. Open a Pull Request

Regenerating the Bundled Dataset #

The default commands use the revisions recorded in THIRD_PARTY_NOTICES.md. Verify or regenerate the vendored data with:

dart run tool/sync_geo_data.dart --check
dart run tool/sync_geo_data.dart
dart run tool/sync_geo_data.dart --ref <commit-or-tag>
dart run tool/sync_geo_data.dart --ref <revision> --input <source.json>

dart run tool/sync_country_data.dart --check
dart run tool/sync_country_data.dart

License #

Derived from Arhamss/countrify; thanks to the original contributors. The original MIT copyright notice is retained in LICENSE.

The package source code is licensed under the MIT License. Bundled databases and translations retain separate upstream terms and attribution; see THIRD_PARTY_NOTICES.md and LICENSES/.


1
likes
160
points
139
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Offline Flutter country, state, and city pickers with emoji flags, localized names, and compact bundled geo data.

Repository (GitHub)
View/report issues

Topics

#country-picker #country #state #city #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on countrify_light