country_kit 1.0.1
country_kit: ^1.0.1 copied to clipboard
Country data + flag widgets for building phone pickers. ISO codes, dial codes, currencies, localized names, phone masks, and SVG flags in circle, square, and rectangle shapes.
import 'package:country_kit/country_kit.dart';
import 'package:country_kit/l10n/fr.dart';
import 'package:flutter/material.dart';
void main() {
// Translations are opt-in: only imported languages ship in the binary.
registerCountryNamesFr();
runApp(const ExampleApp());
}
/// [Continent] ships no display name on purpose — the enum stays free of
/// presentation, and apps map it to whatever their UI needs (localized
/// strings, icons, short codes). This is that mapping for the demo.
extension on Continent {
String get label => switch (this) {
Continent.africa => 'Africa',
Continent.americas => 'Americas',
Continent.antarctic => 'Antarctic',
Continent.asia => 'Asia',
Continent.europe => 'Europe',
Continent.oceania => 'Oceania',
};
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'country_kit demo',
theme: ThemeData(colorSchemeSeed: Colors.teal),
home: const DemoHome(),
);
}
}
class DemoHome extends StatelessWidget {
const DemoHome({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('country_kit'),
bottom: const TabBar(tabs: [
Tab(text: 'Country selector'),
Tab(text: 'Phone picker'),
Tab(text: 'Special flags'),
]),
),
body: const TabBarView(
children: [
CountrySelectorDemo(),
PhonePickerDemo(),
SpecialFlagsDemo(),
],
),
),
);
}
}
/// Searchable country list built from [Countries.search] + [CountryFlag].
class CountrySelectorDemo extends StatefulWidget {
const CountrySelectorDemo({super.key});
@override
State<CountrySelectorDemo> createState() => _CountrySelectorDemoState();
}
class _CountrySelectorDemoState extends State<CountrySelectorDemo> {
final _search = TextEditingController();
String _query = '';
Continent? _continent;
@override
void dispose() {
_search.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Search ignores the continent filter: a query is a stronger signal of
// intent than a chip the user set earlier.
final countries = _query.isNotEmpty
? Countries.search(_query)
: _continent == null
? Countries.all
: Countries.byContinent(_continent!);
return Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: _search,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: 'Search name, code or dial code',
border: OutlineInputBorder(),
),
onChanged: (q) => setState(() => _query = q),
),
),
SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
children: [
for (final continent in Continent.values)
Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(continent.label),
selected: _continent == continent,
onSelected: (on) => setState(() {
_continent = on ? continent : null;
_query = '';
_search.clear();
}),
),
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: countries.length,
itemBuilder: (context, i) {
final c = countries[i];
return ListTile(
leading: CountryFlag(
country: c,
shape: FlagShape.circle,
size: 36,
borderRadius: BorderRadius.circular(1000),
),
title: Text(c.name),
subtitle: Text(
'${c.nativeName ?? c.officialName} · ${c.continent.label}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(c.dialCode ?? ''),
onTap: () => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${c.flagEmoji} ${c.name} — ${c.alpha3}, '
'${c.currencyCode ?? 'no currency'}',
),
),
),
);
},
),
),
],
);
}
}
/// Phone input with a dial-code picker prefix, using rectangle flags and the
/// per-country phone mask/length metadata.
class PhonePickerDemo extends StatefulWidget {
const PhonePickerDemo({super.key});
@override
State<PhonePickerDemo> createState() => _PhonePickerDemoState();
}
class _PhonePickerDemoState extends State<PhonePickerDemo> {
late Country _country = Countries.current ?? Countries.byAlpha2('US')!;
final _phone = TextEditingController();
@override
void dispose() {
_phone.dispose();
super.dispose();
}
Future<void> _pickCountry() async {
final picked = await showModalBottomSheet<Country>(
context: context,
builder: (context) => ListView(
children: [
for (final c in Countries.all.where((c) => c.dialCode != null))
ListTile(
leading: CountryFlag(
country: c,
shape: FlagShape.rectangle,
size: 20,
borderRadius: BorderRadius.circular(3),
),
title: Text(c.name),
trailing: Text(c.dialCode!),
onTap: () => Navigator.pop(context, c),
),
],
),
);
if (picked != null) {
// Re-format what is already typed against the new country's mask.
final digits = digitsOnly(_phone.text);
setState(() {
_country = picked;
_phone.text = picked.formatPhoneNumber(digits);
});
}
}
String? get _error => switch (_country.validatePhoneNumber(_phone.text)) {
PhoneNumberValidity.empty => null,
PhoneNumberValidity.tooShort => 'Too short',
PhoneNumberValidity.tooLong => 'Too long',
PhoneNumberValidity.valid => null,
};
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
inputFormatters: [PhoneInputFormatter.forCountry(_country)],
onChanged: (_) => setState(() {}),
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: _error,
hintText: _country.phoneMask ?? _country.phoneExample,
prefixIcon: InkWell(
onTap: _pickCountry,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 12),
CountryFlag(
country: _country,
shape: FlagShape.rectangle,
size: 18,
borderRadius: BorderRadius.circular(3),
),
const SizedBox(width: 6),
Text(_country.dialCode ?? ''),
const Icon(Icons.arrow_drop_down),
const SizedBox(width: 4),
],
),
),
),
),
const SizedBox(height: 16),
Text('Example number: ${_country.phoneExample ?? '—'}\n'
'Length: ${_country.phoneMinLength ?? '?'}–'
'${_country.phoneMaxLength ?? '?'} digits\n'
'Emoji flag: ${_country.flagEmoji} · '
'French name: ${_country.nameIn('fr')}'),
],
),
);
}
}
/// Searchable list of the extra non-ISO flags (Scotland, EU, UN, ...) —
/// [SpecialFlags.search] + [CountryFlag.fromCode]. Kept separate from the
/// country selector: these are not countries and never appear in
/// [Countries.search].
class SpecialFlagsDemo extends StatefulWidget {
const SpecialFlagsDemo({super.key});
@override
State<SpecialFlagsDemo> createState() => _SpecialFlagsDemoState();
}
class _SpecialFlagsDemoState extends State<SpecialFlagsDemo> {
final _search = TextEditingController();
String _query = '';
@override
void dispose() {
_search.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final flags = _query.isNotEmpty
? SpecialFlags.search(_query)
: SpecialFlags.flags;
return Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
controller: _search,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
hintText: 'Search Scotland, Euskadi, EU…',
border: OutlineInputBorder(),
),
onChanged: (q) => setState(() => _query = q),
),
),
Expanded(
child: ListView.builder(
itemCount: flags.length,
itemBuilder: (context, i) {
final flag = flags[i];
final parent = flag.parentCode == null
? null
: Countries.byAlpha2(flag.parentCode!);
return ListTile(
leading: CountryFlag.fromCode(
flag.code,
shape: FlagShape.rectangle,
size: 24,
borderRadius: BorderRadius.circular(3),
),
title: Text(flag.name),
subtitle: Text(
[
if (flag.nativeName != null) flag.nativeName!,
if (parent != null) 'part of ${parent.name}',
].join(' · '),
),
trailing: Text(flag.code),
);
},
),
),
],
);
}
}