Abraham Golor
06/15/2026, 3:08 PMphonenumber for phone number validation and formatting across Android and iOS.
Why I built it:
While working on a KMP app, I found myself duplicating phone number validation logic across Android and iOS. Android had libphonenumber, iOS had PhoneNumberKit, and keeping behavior consistent between both platforms became messy.
So I built a shared KMP wrapper that exposes a single API while delegating to each platform’s native implementation.
Features:
- Global phone number validation
- Validation by country ISO code
- Validation by country calling code
- Format to E.164
- Format to international format
- Extract region code
- Extract country calling code
- Shared API for Android & iOS
- Native performance (libphonenumber on Android, PhoneNumberKit on iOS)
GitHub: https://github.com/ajayinnovations/phonenumber
Would love feedback on the API design, naming, and any edge cases I might have missed 🙏CLOVIS
06/15/2026, 3:17 PMPhoneNumberInfo as a data class but it's likely that you will add new fields in the future. This is not recommended, see why → https://jakewharton.com/public-api-challenges-in-kotlin/CLOVIS
06/15/2026, 3:18 PMval info = PhoneNumberValidator.parse("+2348012345678")
.copy(international = "whatever ahah")
which makes no sense.CLOVIS
06/15/2026, 3:26 PM== reports them identical, but your existing implementation would report that they are different, so a Set<PhoneNumberInfo> would not work if the input format is different.
To solve all of these problems, you can structure it as:
public class PhoneNumberInfo internal constructor(
val international: String?,
) {
val e164 = PhoneNumberValidator.format(international, PhoneNumberFormat.E164)
val regionCode = PhoneNumberValidator.regionCode(international)
val countryCode = PhoneNumberValidator.countryCode(international)
override fun toString() = "PhoneNumberInfo($international)"
override fun equals(other: Any?): Boolean {
return other is PhoneNumberInfo && international == other.international
}
override fun hashCode(): Int = international.hashCode()
}
// Fake constructor for convenience
fun PhoneNumberInfo(
input: String,
) : PhoneNumberInfo(PhoneNumberValidator.format(input, PhoneNumberFormat.INTERNATIONAL))
You may add by lazy on some fields if they are expensive to compute.
At this point, you may call this type PhoneNumber instead of PhoneNumberInfo , since it contains all info for a phone number.CLOVIS
06/15/2026, 3:28 PMPhoneNumberValidator could be moved into PhoneNumber 's companion object , but it objectively doesn't change much, it's a matter of taste and your preference. The upside is that the entire library can fit in one less public type.Abraham Golor
06/15/2026, 3:35 PMCLOVIS
06/15/2026, 3:48 PM