Skip to content

Added countryComparator to sort countries based on nameLocalized #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/country_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ void showCountryPicker({
bool useRootNavigator = false,
bool moveAlongWithKeyboard = false,
Widget header = const SizedBox.shrink(),
int Function(Country a, Country b)? countryComparator,
}) {
assert(
exclude == null || countryFilter == null,
Expand All @@ -90,5 +91,6 @@ void showCountryPicker({
useRootNavigator: useRootNavigator,
moveAlongWithKeyboard: moveAlongWithKeyboard,
header: header,
countryComparator: countryComparator,
);
}
46 changes: 22 additions & 24 deletions lib/src/country.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ class Country {
///The country name in English
final String name;

///The country name localized
late String? nameLocalized;
///The country name localized. It will be initialized after the Country object is created.
late String nameLocalized;

///An example of a telephone number without the phone code
final String example;
Expand All @@ -55,9 +55,15 @@ class Country {
)
String get displayNameNoE164Cc => displayNameNoCountryCode;

String? getTranslatedName(BuildContext context) {
return CountryLocalizations.of(context)
?.countryName(countryCode: countryCode);
/// Initializes the localized name for the country.
/// Uses the translated name if available, otherwise falls back to the English name.
void initLocalizedName(BuildContext context) {
nameLocalized = CountryLocalizations.of(context)?.countryName(countryCode: countryCode)?.replaceAll(RegExp(r"\s+"), " ") ?? name;
}

/// Initializes the localized name for the 'World Wide' static instance.
static void initWorldWideLocalizedName(BuildContext context) {
worldWide.nameLocalized = CountryLocalizations.of(context)?.countryName(countryCode: worldWide.countryCode)?.replaceAll(RegExp(r"\s+"), " ") ?? worldWide.name;
}

Country({
Expand All @@ -67,7 +73,6 @@ class Country {
required this.geographic,
required this.level,
required this.name,
this.nameLocalized = '',
required this.example,
required this.displayName,
required this.displayNameNoCountryCode,
Expand Down Expand Up @@ -112,6 +117,7 @@ class Country {
data['geographic'] = geographic;
data['level'] = level;
data['name'] = name;
data['nameLocalized'] = nameLocalized;
data['example'] = example;
data['display_name'] = displayName;
data['full_example_with_plus_sign'] = fullExampleWithPlusSign;
Expand All @@ -120,41 +126,33 @@ class Country {
return data;
}

bool startsWith(String query, CountryLocalizations? localizations) {
String _query = query;
if (query.startsWith("+")) {
_query = query.replaceAll("+", "").trim();
/// Checks if the country's properties (name, code, phone code, localized name) start with the query.
/// The [localizations] parameter is no longer used as [nameLocalized] is pre-initialized.
bool startsWith(String query, CountryLocalizations? localizations /* not used anymore */) {
String lowerCaseQuery = query.toLowerCase();
if (lowerCaseQuery.startsWith("+")) {
lowerCaseQuery = lowerCaseQuery.replaceAll("+", "").trim();
}
return phoneCode.startsWith(_query.toLowerCase()) ||
name.toLowerCase().startsWith(_query.toLowerCase()) ||
countryCode.toLowerCase().startsWith(_query.toLowerCase()) ||
(localizations
?.countryName(countryCode: countryCode)
?.toLowerCase()
.startsWith(_query.toLowerCase()) ??
false);
if (lowerCaseQuery.isEmpty) return true;

return phoneCode.startsWith(lowerCaseQuery) || name.toLowerCase().startsWith(lowerCaseQuery) || countryCode.toLowerCase().startsWith(lowerCaseQuery) || nameLocalized.toLowerCase().startsWith(lowerCaseQuery);
}

bool get iswWorldWide => countryCode == Country.worldWide.countryCode;

@override
String toString() => 'Country(countryCode: $countryCode, name: $name)';
String toString() => 'Country(countryCode: $countryCode, name: $name, nameLocalized: $nameLocalized)';

@override
bool operator ==(Object other) {
if (other is Country) {
return other.countryCode == countryCode;
}

return super == other;
}

@override
int get hashCode => countryCode.hashCode;

/// provides country flag as emoji.
/// Can be displayed using
///
///```Text(country.flagEmoji)```
String get flagEmoji => Utils.countryCodeToEmoji(countryCode);
}
14 changes: 7 additions & 7 deletions lib/src/country_list_bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ void showCountryListBottomSheet({
bool useRootNavigator = false,
bool moveAlongWithKeyboard = false,
Widget header = const SizedBox.shrink(),
int Function(Country a, Country b)? countryComparator,
}) {
showModalBottomSheet(
context: context,
Expand All @@ -42,6 +43,7 @@ void showCountryListBottomSheet({
moveAlongWithKeyboard,
customFlagBuilder,
header,
countryComparator,
),
).whenComplete(() {
if (onClosed != null) onClosed();
Expand All @@ -62,15 +64,14 @@ Widget _builder(
bool moveAlongWithKeyboard,
CustomFlagBuilder? customFlagBuilder,
Widget header,
int Function(Country a, Country b)? countryComparator,
) {
final device = MediaQuery.of(context).size.height;
final statusBarHeight = MediaQuery.of(context).padding.top;
final height = countryListTheme?.bottomSheetHeight ??
device - (statusBarHeight + (kToolbarHeight / 1.5));
final height = countryListTheme?.bottomSheetHeight ?? device - (statusBarHeight + (kToolbarHeight / 1.5));
final width = countryListTheme?.bottomSheetWidth;

Color? _backgroundColor = countryListTheme?.backgroundColor ??
Theme.of(context).bottomSheetTheme.backgroundColor;
Color? _backgroundColor = countryListTheme?.backgroundColor ?? Theme.of(context).bottomSheetTheme.backgroundColor;

if (_backgroundColor == null) {
if (Theme.of(context).brightness == Brightness.light) {
Expand All @@ -87,9 +88,7 @@ Widget _builder(
);

return Padding(
padding: moveAlongWithKeyboard
? MediaQuery.of(context).viewInsets
: EdgeInsets.zero,
padding: moveAlongWithKeyboard ? MediaQuery.of(context).viewInsets : EdgeInsets.zero,
child: Container(
height: height,
width: width,
Expand All @@ -116,6 +115,7 @@ Widget _builder(
showWorldWide: showWorldWide,
showSearch: showSearch,
customFlagBuilder: customFlagBuilder,
countryComparator: countryComparator,
),
),
],
Expand Down
131 changes: 76 additions & 55 deletions lib/src/country_list_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class CountryListView extends StatefulWidget {
/// Custom builder function for flag widget
final CustomFlagBuilder? customFlagBuilder;

/// An optional argument for country comparator
final int Function(Country a, Country b)? countryComparator;

const CountryListView({
Key? key,
required this.onSelect,
Expand All @@ -60,6 +63,7 @@ class CountryListView extends StatefulWidget {
this.showWorldWide = false,
this.showSearch = true,
this.customFlagBuilder,
this.countryComparator,
}) : assert(
exclude == null || countryFilter == null,
'Cannot provide both exclude and countryFilter',
Expand All @@ -79,46 +83,81 @@ class _CountryListViewState extends State<CountryListView> {
late TextEditingController _searchController;
late bool _searchAutofocus;
bool _isSearching = false;
bool _dependenciesInitialized = false; // Flag to run didChangeDependencies logic once

@override
void initState() {
super.initState();
_searchController = TextEditingController();
_searchAutofocus = widget.searchAutofocus;

_countryList = _countryService.getAll();

_countryList =
countryCodes.map((country) => Country.from(json: country)).toList();

//Remove duplicates country if not use phone code
if (!widget.showPhoneCode) {
final ids = _countryList.map((e) => e.countryCode).toSet();
_countryList.retainWhere((country) => ids.remove(country.countryCode));
}

if (widget.favorite != null) {
_favoriteList = _countryService.findCountriesByCode(widget.favorite!);
}

if (widget.exclude != null) {
_countryList.removeWhere(
(element) => widget.exclude!.contains(element.countryCode),
);
}
// Initial load of countries (without localized names yet)
_countryList = countryCodes.map((countryData) => Country.from(json: countryData)).toList();
_filteredList = []; // Initialize to avoid late errors before didChangeDependencies
}

if (widget.countryFilter != null) {
_countryList.removeWhere(
(element) => !widget.countryFilter!.contains(element.countryCode),
);
@override
void didChangeDependencies() {
super.didChangeDependencies();

if (!_dependenciesInitialized) {
// Initialize localized names for all countries in the main list
for (final country in _countryList) {
country.initLocalizedName(context);
}

// Initialize localized name for the static 'World Wide' instance if shown
if (widget.showWorldWide) {
Country.initWorldWideLocalizedName(context);
}

// Sort the main list if a comparator is provided
// This is done after nameLocalized is initialized
if (widget.countryComparator != null) {
_countryList.sort(widget.countryComparator);
}

if (!widget.showPhoneCode) {
final ids = _countryList.map((e) => e.countryCode).toSet();
_countryList.retainWhere((country) => ids.remove(country.countryCode));
}

if (widget.exclude != null) {
_countryList.removeWhere(
(element) => widget.exclude!.contains(element.countryCode),
);
}

if (widget.countryFilter != null) {
_countryList.removeWhere(
(element) => !widget.countryFilter!.contains(element.countryCode),
);
}

// Initialize favorites list and their localized names
if (widget.favorite != null && widget.favorite!.isNotEmpty) {
// Assuming _countryService.findCountriesByCode fetches fresh instances
// or instances that might not have initLocalizedName called.
final List<Country> tempFavoriteList = _countryService.findCountriesByCode(widget.favorite!);
for (final favCountry in tempFavoriteList) {
favCountry.initLocalizedName(context); // Ensure localized name is set
}
_favoriteList = tempFavoriteList;
}

// Build the initial _filteredList
_rebuildFilteredList();

_dependenciesInitialized = true;
}
}

_filteredList = <Country>[];
void _rebuildFilteredList() {
_filteredList.clear();
if (widget.showWorldWide) {
_filteredList.add(Country.worldWide);
}
_filteredList.addAll(_countryList);

_searchAutofocus = widget.searchAutofocus;
}

@override
Expand All @@ -141,9 +180,7 @@ class _CountryListViewState extends State<CountryListView> {

@override
Widget build(BuildContext context) {
final String searchLabel =
CountryLocalizations.of(context)?.countryName(countryCode: 'search') ??
'Search';
final String searchLabel = CountryLocalizations.of(context)?.countryName(countryCode: 'search') ?? 'Search';

return Column(
children: <Widget>[
Expand All @@ -152,8 +189,7 @@ class _CountryListViewState extends State<CountryListView> {
TextField(
autofocus: _searchAutofocus,
controller: _searchController,
style:
widget.countryListTheme?.searchTextStyle ?? _defaultTextStyle,
style: widget.countryListTheme?.searchTextStyle ?? _defaultTextStyle,
decoration: widget.countryListTheme?.inputDecoration ??
InputDecoration(
labelText: searchLabel,
Expand All @@ -173,8 +209,8 @@ class _CountryListViewState extends State<CountryListView> {
Expanded(
child: ListView(
children: [
if (_favoriteList != null && !_isSearching) ...[
..._favoriteList!.map<Widget>((currency) => _listRow(currency)),
if (_favoriteList != null && _favoriteList!.isNotEmpty && !_isSearching) ...[
..._favoriteList!.map<Widget>((country) => _listRow(country)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Divider(thickness: 1),
Expand All @@ -189,8 +225,7 @@ class _CountryListViewState extends State<CountryListView> {
}

Widget _listRow(Country country) {
final TextStyle _textStyle =
widget.countryListTheme?.textStyle ?? _defaultTextStyle;
final TextStyle _textStyle = widget.countryListTheme?.textStyle ?? _defaultTextStyle;

final bool isRtl = Directionality.of(context) == TextDirection.rtl;

Expand All @@ -200,9 +235,6 @@ class _CountryListViewState extends State<CountryListView> {
color: Colors.transparent,
child: InkWell(
onTap: () {
country.nameLocalized = CountryLocalizations.of(context)
?.countryName(countryCode: country.countryCode)
?.replaceAll(RegExp(r"\s+"), " ");
widget.onSelect(country);
Navigator.pop(context);
},
Expand All @@ -213,10 +245,7 @@ class _CountryListViewState extends State<CountryListView> {
Row(
children: [
const SizedBox(width: 20),
if (widget.customFlagBuilder == null)
_flagWidget(country)
else
widget.customFlagBuilder!(country),
if (widget.customFlagBuilder == null) _flagWidget(country) else widget.customFlagBuilder!(country),
if (widget.showPhoneCode && !country.iswWorldWide) ...[
const SizedBox(width: 15),
SizedBox(
Expand All @@ -233,10 +262,7 @@ class _CountryListViewState extends State<CountryListView> {
),
Expanded(
child: Text(
CountryLocalizations.of(context)
?.countryName(countryCode: country.countryCode)
?.replaceAll(RegExp(r"\s+"), " ") ??
country.name,
country.nameLocalized, // Use the pre-initialized localized name
style: _textStyle,
),
),
Expand All @@ -257,9 +283,7 @@ class _CountryListViewState extends State<CountryListView> {
}

Widget _emojiText(Country country) => Text(
country.iswWorldWide
? '\uD83C\uDF0D'
: Utils.countryCodeToEmoji(country.countryCode),
country.iswWorldWide ? '\uD83C\uDF0D' : Utils.countryCodeToEmoji(country.countryCode),
style: TextStyle(
fontSize: widget.countryListTheme?.flagSize ?? 25,
fontFamilyFallback: widget.countryListTheme?.emojiFontFamilyFallback,
Expand All @@ -268,15 +292,12 @@ class _CountryListViewState extends State<CountryListView> {

void _filterSearchResults(String query) {
List<Country> _searchResult = <Country>[];
final CountryLocalizations? localizations =
CountryLocalizations.of(context);
final CountryLocalizations? localizations = CountryLocalizations.of(context);

if (query.isEmpty) {
_searchResult.addAll(_countryList);
} else {
_searchResult = _countryList
.where((c) => c.startsWith(query, localizations))
.toList();
_searchResult = _countryList.where((c) => c.startsWith(query, localizations)).toList();
}

setState(() => _filteredList = _searchResult);
Expand Down
Loading