A configurable, reusable Android vehicle plate-number input: country picker, category/letter rules, live visual preview, and validation — built so a default country and a set of "other country" options are configuration, not hardcoded logic.
Originally extracted from a Qatar-based fleet app's Customer Identification screen, where the plate-country picker had grown into ~40% of one fragment's code. Splitting it into a library made that logic testable on its own, and reusable in any project that needs a similar "which country's plate is this" input — GCC-region apps in particular, but the core is not GCC-specific.
The library provides high-fidelity, dimension-accurate renderings for vehicle plates across the GCC region.
- Standard & Long: Standard (33x15), European Long (52x11), and Small formats.
- Transport & Public: Public (90), Public Transport (91), Buses (92), Taxis (93), Goods Exportation (95).
- Security & Specialty: Police, National Guard, Firefighting, Army, Construction, Export.
- Diplomatic & Emiri: Emiri Bureau, Emiri Guard, Diplomatic (with unique red band).
- Motorcycles: Square formats for both Private and Police bikes.
- Comprehensive: Private, Taxi, Police, ISF, Diplomatic, Government.
- Dynamic Rendering: National maroon strip with serrated edge and sub-style specific badges.
- Saudi Arabia: Accurate Palm & Crossed Swords with bilingual 2x2 grid.
- Bahrain: High-detail serrated national flag.
- Oman: Wide format with national seal.
- UAE: Region-specific emirate layouts (Dubai, Abu Dhabi, and more).
- Configurable, not hardcoded. Default country, enabled "other countries," and excluded countries are all set via a builder — no fragment/activity code branches on country.
- Country-agnostic core.
PlateCountryDefinitionis an open data class, not a closed enum, so any country's plate rules can be added without touching library internals. - Zero host-resource coupling.
platekit-androidships its own colors/drawables/strings; it never reaches into a consuming app's resources, so it drops into any project as-is. - Live visual preview. Country-specific plate templates (including Qatar's taxi/police/diplomatic/government sub-styles) render as the user types.
- Dynamic UI hooks. Use
setOnCountryChangeListenerto adapt your host app's UI (like showing/hiding extra vehicle-type options) as the user switches between countries. - Fully unit-tested logic.
platekit-corehas no Android dependency, so the build/validate rules are tested as plain JVM code, no emulator required.
platekit-core— pure Kotlin/JVM, zero Android dependency. Country definitions, category rules (single dropdown / two letters / three letters / none), the catalog/builder that describes one deployment's configuration, the plate-number builder + validator, and the visual-template resolver. Fully unit tested.platekit-android— thePlateInputViewwidget (country/category pickers, live preview, plate-number field) plus the supportingVehiclePlateTemplateView/GccPlatePreviewViewrendering views. Ships its own colors/drawables/strings so it never depends on a host app's resources.sample— a minimal standalone demo Activity (Qatar default).dummy-sample— a second demo app showing how to default to Kuwait and handle dynamic UI changes (com.example.platesample.dummy).
binding.plateInputView.configure(
PlateCountryCatalog.builder()
.defaultCountry(PlateCountries.QATAR) // primary/simple mode
.enableCountries(*PlateCountries.gcc.toTypedArray()) // "other country" picker
.excludeCountries(PlateCountries.JORDAN, PlateCountries.EGYPT) // optional: exclude multiple
.unlistedCountryPolicy(UnlistedCountryPolicy.GENERIC_FALLBACK)
.build()
)
binding.searchButton.setOnClickListener {
when (val result = binding.plateInputView.getFormattedPlateNumber()) {
is PlateInputResult.Valid -> submit(result.plateNumber)
is PlateInputResult.Invalid -> showError(result.message)
}
}Switching the default country for a different deployment (e.g. Qatar → Kuwait) is a
one-line change to defaultCountry(...) — no code elsewhere needs to change.
Bahrain, Saudi Arabia, Kuwait, UAE, Qatar, Oman, Egypt, Jordan (PlateCountries.gcc).
PlateCountryDefinition is an open data class, not a closed enum — add your own for any
country not covered here.
platekit-android's views are optional convenience on top of platekit-core, which has
zero Android dependency. If you want your own fields in your own layout with your own
colors, skip PlateInputView entirely and call platekit-core directly — it's the exact
same code the widget itself calls, so your custom UI stays just as accurate:
val country = PlateCountries.OMAN
val template = country.templateResolver.resolve(region = "", vehicleTypeCode = "", vehicleTypeName = "")
// country.categoryMode, country.categoryOptions(), template.numberMaxLength drive your own fields
// Oman's letter field may hold a Latin letter, an Arabic letter, or the "None" sentinel --
// always normalize before validating/building. Same function PlateInputView uses internally,
// so a future fix to it reaches your custom UI automatically.
val letters = if (country.code == "OMN") PlateCountries.canonicalOmanLetter(rawLetterFieldText) else rawLetterFieldText
// Your own plate-number EditText should reuse the library's digits-only restriction too --
// every GCC plate number is digits-only, verified per country.
numberEditText.inputType = android.text.InputType.TYPE_CLASS_NUMBER
numberEditText.filters = PlateInputFilters.digitsOnly(template.numberMaxLength)
val result = PlateNumberValidator.validate(
selectedCountry = country, rawNumber = "1234", maximumLength = template.numberMaxLength, selectedLetters = letters
)
if (result is PlateValidationResult.Valid) {
val formatted = PlateNumberBuilder.build(country, rawNumber = "1234", selectedLetters = letters)
}| Class / object | Members |
|---|---|
PlateCountries |
gcc: List<PlateCountryDefinition> — Bahrain, Saudi Arabia, Kuwait, UAE, Qatar, Oman, Egypt, Jordan. Also exposes the built-in constants directly: QATAR, BAHRAIN, SAUDI_ARABIA, KUWAIT, UAE, OMAN, EGYPT, JORDAN. uaeStates: List<String> (7 emirates). singleLetters: List<String> (A-Z). omanLetterArabicByLatin / omanLetterLatinByArabic: Map<String,String> for Oman's dual-script category letters. OMAN_NO_LETTER_OPTION: String — the "clear this letter" sentinel offered in Oman's category dropdown. canonicalOmanLetter(value: String): String — normalizes a raw Oman letter field (Latin, Arabic, the "None" sentinel, or blank) to the canonical Latin code; the one shared place this conversion lives, call it before validating/building if you're not using PlateInputView. |
PlateCountryDefinition |
Data class describing one country's rules: code, displayName, categoryMode: PlateCategoryInputMode, categoryOptions(): List<String>, regionOptions: List<String> (UAE only), flagKey, templateResolver: PlateTemplateResolver, countryId, nameAr. Not a closed enum — construct your own for a country outside the built-in set. |
PlateCategoryInputMode |
Enum: NONE (digits only, e.g. Qatar's legacy no-category mode), SINGLE_DROPDOWN (one value from categoryOptions(), e.g. Kuwait 1-99, Jordan 100-999 + PM/GV/CD/PR/SN, UAE letter/AA-FF/1-55, Qatar Q/T/R, Bahrain PRIVATE/DIPLOMAT/FOR HIRE), TWO_LETTERS (Oman — 2nd letter optional), THREE_LETTERS (Saudi Arabia, Egypt — all 3 required). |
PlateTemplateResolver |
fun interface: resolve(region, vehicleTypeCode, vehicleTypeName): VehiclePlateTemplate. Every PlateCountryDefinition carries one by default via VehiclePlateTemplates.resolve. |
PlateCountryCatalog |
Configuration for one deployment. Build via PlateCountryCatalog.builder(): .defaultCountry(country) (required), .enableCountries(vararg/Collection), .excludeCountries(vararg) / .excludeCountryCodes(vararg) (always wins over enabling), .unlistedCountryPolicy(UnlistedCountryPolicy), .build(). Instance: defaultCountry, enabledCountries, visibleCountries (default + enabled, deduped), findByCode(code): PlateCountryDefinition?. |
UnlistedCountryPolicy |
Enum: GENERIC_FALLBACK (still searchable, generic plate visual) or HIDDEN (not offered), for any country not explicitly enabled. |
PlateNumberValidator |
validate(selectedCountry, rawNumber, maximumLength, selectedCategoryCode = "", selectedLetters = "", selectedRegion = ""): PlateValidationResult. selectedCountry == null always returns Valid (legacy default-country pass-through). Otherwise: digits-only + within maximumLength, category/letters required per categoryMode (Oman needs only the first letter), region required if regionOptions is non-empty. |
PlateValidationResult / PlateValidationReason |
Valid or Invalid(reason). Reasons: NUMBER_INVALID_OR_TOO_LONG, CATEGORY_NOT_SELECTED, LETTERS_INCOMPLETE, REGION_NOT_SELECTED — map each to your own localized message. |
PlateNumberBuilder |
build(selectedCountry, rawNumber, selectedCategoryCode = "", selectedLetters = ""): String — the final string for a search/verify API. selectedCountry == null returns the raw digits untouched; otherwise category/letters are prefixed, joined by -. |
VehiclePlateTemplates |
resolve(countryCode, region = "", vehicleTypeCode = "", vehicleTypeName = ""): VehiclePlateTemplate — the country/vehicle-type-specific colors, badge text, and numberMaxLength (includes Qatar's private/taxi/police/diplomatic/government sub-styles and Kuwait's 16 sub-templates, matched by vehicle-type string). |
VehiclePlateTemplate |
Data class: layout: VehiclePlateLayout, backgroundColor/textColor/badgeColor/badgeTextColor (ARGB Int), badgeTop/badgeBottom/headerText/footerText (String), headerColor/headerTextColor/footerTextColor, showCategory: Boolean, categoryTextColor, numberMaxLength: Int (the real per-country/plate-type digit cap — this is the source of truth, not a UI constant), widthDp/heightDp. |
VehiclePlateLayout |
Enum of visual layouts the renderer understands: QATAR, QATAR_DIPLOMATIC, QATAR_POLICE, QATAR_ISF, SAUDI, UAE_DUBAI, UAE_ABU_DHABI, UAE_EMIRATE, KUWAIT, KUWAIT_LONG, KUWAIT_SMALL, KUWAIT_SQUARE, KUWAIT_DIPLOMATIC, BAHRAIN, OMAN, GENERIC. |
| Class | Members |
|---|---|
PlateInputView |
configure(catalog: PlateCountryCatalog) — call once before anything else. setOnCountryChangeListener(listener: (PlateCountryDefinition?) -> Unit) — fires whenever the selected country changes (either mode). setModeIcons(primaryIconRes, alternateIconRes), setVehicleType(code, name) (drives which visual sub-template renders), setInputEnabled(enabled), rawNumber(): String, setRawNumberFocusChangeListener(listener), reset(), getFormattedPlateNumber(): PlateInputResult — validates (alternate mode only) then builds the final string. |
PlateInputResult |
Valid(plateNumber, countryCode?, countryName?, countryId?, countryNameAr?) (last four populated only in alternate/GCC mode) or Invalid(message) (already localized via the view's string resources). |
VehiclePlateTemplateView (open) |
The actual plate-drawing custom View — usable completely standalone, anywhere in your own layout. render(countryCode, region = "", vehicleTypeCode = "", vehicleTypeName = "", categoryValue = "", plateNumber = "") or render(template: VehiclePlateTemplate, categoryValue, plateNumber) for a pre-resolved template. currentNumberMaxLength: Int (read-only) reflects the last-rendered template's digit cap. |
GccPlatePreviewView |
Compatibility subclass of VehiclePlateTemplateView with an older render(flagRes, countryCode, countryName, category, plateNumber, showCategory) signature; delegates straight through. |
FlagProvider |
fun interface: flagFor(countryCode): Int? — supply your own flag drawable per country without this library ever shipping or referencing your resources. |
PlateInputFilters |
digitsOnly(maxLength: Int): Array<InputFilter> — the same digits-only + max-length InputFilters PlateInputView applies to its own number field. Set on your own custom plate-number EditText (with inputType = InputType.TYPE_CLASS_NUMBER) to get identical, future-proof behavior without reimplementing the filter. |
- JDK 1.8+ (Configured with
jvmTarget = "1.8"for maximum compatibility across environments). - Android SDK 34 (Compile SDK).
Early extraction, not yet published to Maven/JitPack. Package name (com.developer.platekit)
and API are still subject to change before a 1.0 release.
Issues and pull requests are welcome — especially new PlateCountryDefinitions for
countries outside the current GCC set. No formal process yet; open an issue to discuss
anything non-trivial before sending a PR.
Apache License 2.0 — see LICENSE.