Getting started¶
Requirements¶
| Platforms | Android (min SDK 24, Android 7.0) and iOS (iosArm64, iosSimulatorArm64) |
| UI toolkit | Compose Multiplatform with Material 3 — or Jetpack Compose in an Android-only app |
| Kotlin | A Kotlin 2.x toolchain (the library is built with Kotlin 2.4) |
| Transitive dependencies | Google libphonenumber on Android and kotlinx-coroutines-core. On iOS the phone-number engine, libPhoneNumber-iOS, is compiled into the library. No DataStore, no image-loading library, no material-icons-extended. |
Flags are rendered as the platform's emoji glyphs, so the library adds no flag bitmaps to your app.
Installation¶
The library is on Maven Central, which most projects already list in settings.gradle.kts.
Gradle resolves the right artifact for each target — ccp-android, ccp-iosarm64 or
ccp-iossimulatorarm64.
iOS¶
There is nothing to add on the Xcode side. libPhoneNumber-iOS is compiled into the library's iOS
klib under a CCP symbol prefix, so no CocoaPod, Swift package or linker flag is needed, and an app
that also links libPhoneNumber-iOS itself does not hit duplicate symbols.
Show the components from your iOS source set like any other Compose Multiplatform UI:
fun MainViewController(): UIViewController = ComposeUIViewController {
var country by remember { mutableStateOf<Country?>(null) }
CountrySelector(selectedCountry = country, onCountrySelected = { country = it })
}
The repository's iosApp/ is a working Xcode project around the shared sample.
JitPack (Android only)¶
JitPack also builds the library, but on Linux, which cannot produce the iOS artifacts — use it only from Android-only projects:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven("https://jitpack.io")
}
}
// build.gradle.kts
dependencies {
implementation("com.github.EzekielWachira:Country-Code-Picker:<LATEST_VERSION>")
}
Experimental Material 3 APIs
The library opts in to ExperimentalMaterial3Api and ExperimentalFoundationApi internally
(for ModalBottomSheet and sticky headers). You do not need to add any opt-in to consume it.
Your first country selector¶
import com.ezzy.ccp.countrypicker.model.Country
import com.ezzy.ccp.countrypicker.model.UiText
import com.ezzy.ccp.countrypicker.ui.CountrySelector
@Composable
fun ResidenceField() {
var country by rememberSaveable { mutableStateOf<Country?>(null) }
CountrySelector(
selectedCountry = country,
onCountrySelected = { country = it },
label = UiText.of("Country of residence"),
)
}
CountrySelector is a controlled component: selectedCountry is the source of truth, the
component never mutates it, and a selection only ever arrives through onCountrySelected. The
callback carries the complete Country (ISO alpha-2 and
alpha-3, dial code, region, flag), never just a name or a code.
Hold the selection in rememberSaveable or a ViewModel
The sheet's transient state (query, region filter, pending selection) is saved and restored for you. The confirmed selection is yours, so keep it somewhere that survives configuration changes and process death.
Your first phone number field¶
import com.ezzy.ccp.countrypicker.data.DefaultCountryDataSource
import com.ezzy.ccp.countrypicker.model.PhoneNumberValue
import com.ezzy.ccp.countrypicker.state.rememberPhoneNumberFieldState
import com.ezzy.ccp.countrypicker.ui.PhoneNumberField
@Composable
fun PhoneField(onChange: (PhoneNumberValue) -> Unit) {
val kenya = remember { DefaultCountryDataSource.findByIso2("KE")!! }
val phoneState = rememberPhoneNumberFieldState(initialCountry = kenya)
PhoneNumberField(
state = phoneState,
onValueChange = onChange,
)
Button(
enabled = phoneState.value.isValid,
onClick = { submit(phoneState.value.e164Number!!) },
) { Text("Continue") }
}
PhoneNumberValue.e164Number comes from libphonenumber and is null (not an empty string) while the
number cannot be parsed. Never rebuild it by concatenating a dial code onto digits; that is wrong for
every country with a national trunk prefix (a UK mobile typed as 07400 123456 is +447400123456,
not +4407400123456).
Pass an initial country
If you omit state, the field starts on the first country in the dataset alphabetically, which is
Afghanistan. Pass rememberPhoneNumberFieldState(initialCountry = …) with a sensible default, or
combine it with country detection.
Where to go next¶
-
Pick a selector variant
Full-width, compact pill, flag-only or dial-code.
-
Restrict, group and search
Allowed and excluded countries, region filters, suggested and recent sections.
-
Match your theme
Colors, shapes, dimensions, typography, motion and custom rows.
-
Already using
PhoneNumberInput?
It still works unchanged. Migrate only when you want the newer features.