Country detection¶
Detection pre-fills a likely country from device signals so most users never open the sheet.
CountrySelector(
selectedCountry = country,
onCountrySelected = { country = it },
detector = rememberDefaultCountryDetector(),
detectionBehavior = CountryDetectionBehavior.ShowBadge,
)
Three rules the library relies on:
- No location permission. Country-level detection never justifies a runtime permission prompt.
DefaultCountryDetectorreads only the SIM and network country codes and the locale on Android, and the Region setting on iOS — all permission-free. - Never called from composition.
CountryDetector.detectCountry()issuspendand runs from aLaunchedEffect, so a network-backed detector can take as long as it needs. - Detection is a suggestion, not a command. An explicit user selection always outranks a detected one. A late-arriving lookup can never silently contradict a country the user already tapped, and a detected code the configuration excludes is ignored rather than forced into the field.
DefaultCountryDetector¶
// Common code — builds the right one for the platform.
val detector = rememberDefaultCountryDetector(fallbackIso2Code = "US")
// Android, outside composition (any context; the application context is fine)
DefaultCountryDetector(context, fallbackIso2Code = "US")
// iOS, outside composition
DefaultCountryDetector(fallbackIso2Code = "US")
On Android it tries, in order of confidence: SIM country → network country → configuration locale →
default locale → fallback. iOS has no usable SIM or network country API (CoreTelephony's carrier
properties have returned placeholder values since iOS 16), so it reads the Region setting →
fallback — an explicit user choice in Settings › General › Language & Region, which makes it a
better signal than a locale derived from a language. Pass fallbackIso2Code = null to report Unavailable instead of guessing, which
is the right choice when a wrong default is worse than no default. Malformed codes some devices report
("", "--", three-letter codes) are validated against the dataset and dropped.
The locale is the weakest signal (plenty of people run an English (US) device in Nairobi), so it is reported as "Based on your device language" rather than as a location claim.
Behaviours¶
CountryDetectionBehavior |
What the picker does |
|---|---|
ShowBadge (default) |
Applies the country and shows a tonal badge naming the source: "✓ Detected · From your SIM card · tap to change" |
AskFirst |
Does not apply anything. Surfaces the result as a suggestion the user accepts explicitly. Right for fields with legal weight (tax residency, sanctions screening), where a silently prefilled answer the user never gave is a liability |
Silent |
Applies the country with no visible explanation |
The two visible treatments are also available as standalone composables for hosts composing their own
layout: DetectedCountryBadge(source) and DetectedCountrySuggestion(country, source, onAccept).
Result and source types¶
sealed interface CountryDetectionResult {
data object Idle : CountryDetectionResult
data object InProgress : CountryDetectionResult // selector shows its Loading treatment
data class Detected(val iso2Code: String, val source: CountryDetectionSource) : CountryDetectionResult
data object Unavailable : CountryDetectionResult // nothing determined; not an error
}
Detected carries an ISO code rather than a Country, so detectors need no access to the dataset.
The picker resolves the code against whatever data source is configured.
CountryDetectionSource |
Meaning |
|---|---|
UserSelection |
The user picked it. Never overridden |
SavedSelection |
Restored from a previous session |
ActiveProfile |
Supplied by the host from the signed-in profile |
Sim |
The SIM card's registered country. Strongest device signal |
Network |
The attached network, or a host-provided IP lookup |
Locale |
The device language/region. Weakest |
Default |
A hardcoded fallback |
Each source has a label: UiText used for the badge's supporting text.
Custom detectors¶
CountryDetector is a fun interface, so a one-off detector is a lambda. The idiomatic way to add a
backend or IP lookup without giving up the device signals is withFallback:
val detector = CountryDetector {
api.geoCountry()?.let { CountryDetectionResult.detected(it, CountryDetectionSource.Network) }
?: CountryDetectionResult.Unavailable
}.withFallback(DefaultCountryDetector(context)) // or DefaultCountryDetector() on iOS
For previews and tests, staticCountryDetector("KE") always reports the given code.
Reading the result¶
CountryPickerState exposes detectionResult and detectionSource; the latter is null once the
user has made an explicit choice. When using rememberCountryPickerState directly, pass
onDetectedCountry to adopt the detected country into your own state. It is not called when the user
has already chosen, or when the behaviour is AskFirst.