Fun with Flags Redux

Barry Irvine
Go City Engineering
4 min readMay 15, 2024

Way back in the murky mists of time (around the time of Trump’s first(!) presidential win) I wrote two articles about creating a country picker with flags for each country in Android.

In those days we daubed the cave walls of our code base in Java and rendered our prehistoric views in XML. Android versions were still named after desserts and candy and we were all aflutter about the inline notifications of Android Nougat. Now of course we’re all using Kotlin and (hopefully) Jetpack Compose and most of us don’t even support Android Nougat any more. It’s not just the tools I used then that startle me but other limitations of the solution are quite apparent.

Firstly, why did I have a list of all the country codes and then resource strings for each country code with the translations for every country name? Java has a lovely method to give me all the country codes in one go. For each of those country codes I can then get the name of that country in a given language just by passing a Locale for that language.

Locale.getISOCountries() // Returns an array of ISO-3166 2 letter country codes

/**
Get the name of a country code for a given Locale - in this case the default one
*/
val countryName = Locale("", countryCode).getDisplayCountry(Locale.getDefault())

If we define a data class for Country we can map all the countries and their names in one go.

data class Country(val isoCode: String, val name: String)
fun getAllCountries(locale: Locale = Locale.getDefault()): List<Country> =
Locale.getISOCountries().map {
Country(it, Locale("", it).getDisplayCountry(locale))
}

By default this takes the user’s current Locale but (especially for testing purposes) I can pass in another Locale and it will give me the names for that language.

val countries = getAllCountries(Locale.FRANCE)
assertThat(countries.find { it.isoCode == "ZA" }).isEqualTo("Afrique du Sud")

No more translations! It all just works out of the box!

I also wanted to order the countries by their name in that language. One immediate problem was with accents, even in English we have “Åland Islands”. The default A-Z sorting would place this last in the list, instead of 2nd after Afghanistan and before Albania! To handle this I created a remove diacritics extension function and can then use this when sorting the list of countries.

private val DIACRITICS = Regex("\\p{InCombiningDiacriticalMarks}+")

internal val String.removeDiacritics
get() = Normalizer.normalize(this, Normalizer.Form.NFD).replace(DIACRITICS, "")

Finally, I think it’s quite useful to put the user’s current country at the top of the list (most likely the one that they’re going to use when picking a country for an address etc). So the complete method now looks like this:

fun getAllCountries(locale: Locale = Locale.getDefault()): List<Country> =
Locale.getISOCountries().map {
Country(it, Locale("", it).getDisplayCountry(locale))
}
// Put user's current locale's country at the top of the list
.partition { it.isoCode == locale.country }
.let { (currentCountry, remainingCounties) ->
currentCountry + remainingCounties.sortedBy { it.name.removeDiacritics }
} // Ensures that countries with accents in the name are sorted correctly

So now I have a list of countries, in the language I want and ordered alphabetically for that language but, to make our picker really sing, we also want to display the country’s flag next to the name.

In my second article 7 years ago I tried to add the emoji flag next to the country but this had limited success because not all flags were supported by all Android versions and were handled differently by different OEMs. Fortunately 7 years is a geological age in mobile development, not only have we had 7 new versions of Android but the emojis have been largely decoupled from the Android version that the user is running. So I was able to use my original logic (now re-written in Kotlin) and it works for all 249 countries! I simply add this as a property to my Country data class.

val flag = isoCode.map { it.getRegionalIndicatorSymbol() }.joinToString("")

companion object {
/**
* The regional indicators go from 0x1F1E6 (A) to 0x1F1FF (Z).
* This is the A regional indicator value minus 65 decimal so
* that we can just add this to the A-Z char
*/
private const val REGIONAL_INDICATOR_OFFSET = 0x1F1A5

fun Char.getRegionalIndicatorSymbol(): String {
check(this in 'A'..'Z') { "Invalid character: you must use A-Z" }
return String(Character.toChars(REGIONAL_INDICATOR_OFFSET + code))
}

Another difference between now and then is how much easier it is to render a list on screen. In Compose it’s a matter of a few lines of code, rather than a RecyclerView, an XML item layout, an adapter and diff utils etc. My original plan was to use an ExposedDropDownMenu but the performance for those are appalling once you have 100+ rows because it instantiates every element in a Column rather than recycling the elements lazily. Therefore I opted for showing all the countries in a LazyList within a bottom sheet.

Country picker in Korean

The complete code for this, including the LargeBottomMenu implementation can be found on GitHub here. I have also added it as a maven library so you just add it straight to your project without even looking at the code.

implementation("com.gocity.countrypicker:countrypicker:1.0.0")

For most use cases you’ll probably just need the below code and then you can do what you want with the country value once set.

var country: Country? by rememberSaveable { mutableStateOf<Country?>(null) }
CountryPicker(currentCountry = country) {
country = it
}

As usual, if you enjoyed this post, I’d appreciate some claps, a follow, share or tweet and if you have any questions please don’t hesitate to post them below.

--

--

Barry Irvine
Go City Engineering

Writing elegant Android code is my passion — but with 20+ years experience in roles from programme delivery to working at the coal face, I’ve seen it all.