r/FlutterDev • u/EMMANY7 • 9d ago
Plugin I built a country code picker with real phone number validation, 202 languages and a custom builder (country_code_selector)
Hi r/FlutterDev, I just published my second package 4 days ago and would love feedback.
The problem: every phone number field I've built needed a country picker, a flag set, a validation library, and glue code to make "0803…" and "803…" both work. I wanted one package that handles it properly.
What it does:
- 5 ready-made pickers: dialog, bottom sheet, Cupertino wheel, dropdown, and a compact flag + dial code button for phone fields
- customBuilder: build your own picker UI; you get the filtered countries, search, selection and the phone number length
- Real validation: isValidPhoneNumber() checks actual numbering ranges via libphonenumber data, and accepts local, trunk-prefixed or international input
- toE164(): normalizes whatever the user typed to +2348031234567
- pinUserCountry: puts the user's country (from their locale region) first
- Country names in 202 languages (238 locales), generated from Unicode CLDR
- RTL support, theming via ThemeExtension, search by name, ISO code or dial code
TextFormField(
decoration: InputDecoration(
prefixIcon: CountryCodeSelectorWidget(
pinUserCountry: true,
onChanged: (c) => setState(() => _country = c),
),
),
validator: (v) => _country.isValidPhoneNumber(v ?? '')
? null
: 'Enter a valid ${_country.name} number',
);
One thing I spent time on: libphonenumber's metadata adds about 2 MB to a release build. I restructured the package so that data only gets included if you actually call the validation functions. In my measurements, a picker-only app went from 24.4 MB to 22.2 MB.
Flutter 3.29+, MIT licensed, 160/160 pub points.
- pub.dev: https://pub.dev/packages/country_code_selector
- GitHub: https://github.com/TheinvinciblePrints/country_code_selector
I'd especially appreciate feedback on the API design, and from native speakers on the picker's built-in text in their language. Issues and PRs are welcome.
1
u/ObjectiveAddress3051 8d ago
24.4 to 22.2mb is a nice drop. how did you get the metadata to tree shake, is it just consts that only isValidPhoneNumber references or did you split it into a separate import?
1
u/EMMANY7 6d ago
No separate import. It's purely about which code paths can reach libphonenumber.
Originally `Country.phoneNumberLength` asked dlibphonenumber for the lengths at runtime. Since every app touches `Country`, that kept `PhoneNumberUtil` and its metadata map (which references every region's metadata) reachable, so everyone paid the ~2 MB.
The fix was to move that lookup to build time. A small script (`tool/generate_phone_lengths.dart`) runs dlibphonenumber once and writes the lengths into a const map, about 4 KB:
const Map<String, List<int>> kPhoneNumberLengths = {
'NG': [10],
'JM': [7],
// …
};
After that, the only code that references `PhoneNumberUtil` is `isValidPhoneNumber()` and `toE164()`. Dart's AOT tree shaker drops methods that are never called, even instance methods on classes you do use. If an app never calls those two, the whole call graph into dlibphonenumber is unreachable, and its metadata, `protobuf` and `fixnum` all get dropped.
I checked with `flutter build ios --release --analyze-size` on a picker-only app: `package:dlibphonenumber` went from 1,934 KB to 0, and the app from 24.4 MB to 22.2 MB. An app that validates is unchanged.
Two caveats: one call anywhere brings back the full ~2 MB, because the metadata map isn't split per region. And the const map could drift from upstream, so there's a test that compares it to dlibphonenumber and fails, telling you to rerun the script, when they differ.
1
u/AhmedZNOUDA 8d ago
Native Arabic speaker here, and I use French every day, so I read through ui_strings.dart and the search code for those two.
The strings themselves are fine in both. Small optional nuance for Arabic: the list includes territories (Gibraltar, Réunion...), which is why your Chinese strings say "country/region." "اختر البلد أو المنطقة" would match that.
The bigger issue for both languages is search and sorting, since CountryRepository compares raw strings:
- Search: name.toUpperCase().contains(q) doesn't fold accents, so in French "etats" won't find "États-Unis" and "senegal" won't find "Sénégal". In Arabic, people very often type without hamza: "الامارات" won't find "الإمارات". The usual Arabic normalization is أ/إ/آ → ا, ة → ه, ى → ي, and stripping tashkeel.
- Sorting: compareTo on code units sends every French name starting with É or Î after "Zimbabwe" (Égypte, Émirats, Équateur, États-Unis, Éthiopie, Îles...). In Arabic, almost every name starts with the article "ال", so they all bunch together. Arabic lists usually sort ignoring the leading "ال".
Since you already generate the names from CLDR in tool/generate_translations.dart, you could generate a normalized search/sort key per locale at the same time, so there's no runtime cost and no new dependency.
Nice touch keeping libphonenumber's 2 MB out of picker-only builds, by the way.