Skip to content

Phone validation

Yakcov ships two phone rules. Pick based on whether you need the libphonenumber dependency:

Rule Dependency Checks
PhoneFormat none Lenient "looks like a phone number" format gate: optional leading +, digits and ( ) . / - separators, 7–15 digits.
Phone libphonenumber-kotlin (compileOnly) Region-aware validity via Google's libphonenumber.

Default: PhoneFormat (no setup)

PhoneFormat has no dependencies and runs on every target. Use it when the server is authoritative (normalizes to E.164 / verifies with a provider, e.g. Telnyx Verify) and the client only needs a cheap format gate:

@Composable
fun PhoneFormatField() {
    // PhoneFormat is a lenient, dependency-free format check — no libphonenumber needed.
    // The server stays authoritative and normalizes to E.164.
    val phone = rememberTextFieldValueValidator(rules = listOf(Required, PhoneFormat))
    with(phone) {
        OutlinedTextField(
            value = value,
            onValueChange = ::onValueChange,
            label = { Text("Phone") },
            isError = isError(),
            supportingText = supportingText(),
            singleLine = true,
            modifier = Modifier
                .fillMaxWidth()
                .validationConfig(validateOnFocusLost = true),
        )
    }
}

It deliberately accepts a wide range of real-world formats (international +, IDD 00/011 prefixes, national trunk 0, spaces/dashes/dots/parens) and leaves authoritative checks to the server. It does not reject wrong-region or structurally-invalid-but-well-formed numbers — that's Phone's job. The underlying check is also available directly as the public String?.isPhoneNumberFormat() helper.

Region-aware: Phone (opt-in)

Phone validates with libphonenumber-kotlin (luca992's multiplatform port of Google's libphonenumber). Yakcov declares it compileOnly, so this rule needs your app (or shared module) to provide the dependency:

commonMain {
    dependencies {
        implementation("io.github.luca992.libphonenumber-kotlin:libphonenumber:0.1.9")
    }
}
@Composable
fun PhoneField() {
    // Phone(defaultRegion) is powered by libphonenumber-kotlin, which yakcov declares
    // compileOnly — your app must add the dependency (see this page's install section).
    val phone = rememberTextFieldValueValidator(rules = listOf(Required, Phone("US")))
    with(phone) {
        OutlinedTextField(
            value = value,
            onValueChange = ::onValueChange,
            label = { Text("Phone") },
            isError = isError(),
            supportingText = supportingText(),
            singleLine = true,
            modifier = Modifier
                .fillMaxWidth()
                .validationConfig(validateOnFocusLost = true),
        )
    }
}

Phone also accepts a State<String> for the region, so the default region can react to a country picker: Phone(defaultRegion = countryState).

Without the dependency, using Phone fails at runtime with a missing-class error — everything else in Yakcov (including PhoneFormat) works without it.

Testing Phone on Android

In an Android local unit test (plain JVM or Robolectric) Phone reports every number invalid until you configure it. On Android the PhoneNumberUtil is built from an application Context captured by an androidx.startup provider, and libphonenumber reads its metadata through compose-resources — a local unit test creates neither ContentProvider (robolectric#9603, CMP-6612).

This fails silently: isPhoneNumber degrades to false rather than throwing, so assertFalse("abc".isPhoneNumber()) passes whether or not validation actually works, while the positive path is never exercised. One call fixes it:

class PhoneFieldTest {
    @Before fun setUp() = initPhoneNumberUtilForTest()
    @After fun tearDown() = resetPhoneNumberUtilForTest()

    @Test fun acceptsAFrenchNumber() {
        assertTrue("+33 1 42 68 53 00".isPhoneNumber("FR"))
    }
}

initPhoneNumberUtilForTest() needs no Context and loads libphonenumber's real metadata, so every region validates. It only requires that your module keeps Android resources available to unit tests — which is also what Robolectric needs:

android { testOptions { unitTests { isIncludeAndroidResources = true } } }

Call resetPhoneNumberUtilForTest() from @After. The util is a per-JVM singleton, so without it a configured test class silently decides whether a later class that forgot to configure passes — making a suite's result depend on execution order within the shard.

To manage metadata loading yourself, pass your own instance: initPhoneNumberUtilForTest(PhoneNumberUtil.createInstance(myLoader)).

Other targets need no setup: JVM, iOS, JS and WasmJS resolve metadata without a Context. Android instrumented tests need no setup either, since androidx.startup runs there.

Roll your own

Need a specific national format or an extension field? Write a ValueValidatorRule<String> — see custom rules. The dependency-free String?.isPhoneNumberFormat() helper is public and reusable as a building block.