TextInputLayout is a wrapper around the OutlinedTextField composable of Material library. It provides a very useful but important feature - Input validation, which is absent in the base composable.
TextInputLayoutDemo.mp4
Features / Edge cases handled :
-
Start with no errors initially
-
Validate length and required checks as and when input changes
-
Optionally prevent further input after maxLength limit is reached
-
Hide "Required!" error as soon a input is added
-
Default IME Actions (Next, Done)
-
Visual Transformation & visibility toggle for Password inputs
Usually you would define a MutableState<String> for an OutlinedTextField, but for a TextInputLayout, define a MutableState<TextInputState> :
val nameInput = remember {
mutableStateOf(
TextInputState(label = "Name")
)
}Observe that
labelis a part of state. Moving ahead you will find more such OutlinedTextField's parameters as a property ofTextInputStateclass.
Composable for this state :
TextInputLayout(state = nameInput)Yeah, it's that simple - just a single line. In contrast, this is how you would define an OutlinedTextField :
val name = remember {
mutableStateOf("")
}
OutlinedTextField(
value = name.value,
onValueChange = { name.value = it },
label = {
Text(
text = "Name"
)
}
)The TextInputState class constructor takes in a InputConfig parameter, using which you can define validation related params & the KeyboardType to be used.
data class TextInputState(
val label: String,
val supportingText: String? = null,
val value: String = "",
val error: String? = null,
val inputConfig: InputConfig = InputConfig.text() // Controls validation & input type
)The default InputConfig is set to text() :
fun InputConfig.Companion.text(): InputConfigWhich uses the default values for InputConfig properties :
class InputConfig {
var optional = false
var keyboardType: KeyboardType = KeyboardType.Text
var minLength = if (optional) 0 else 1
var maxLength = Int.MAX_VALUE
var strictMaxLengthCheck = false
var regexValidation: RegexValidation? = null
}You can customize the InputConfig by passing a lambda like this :
val nameInput = remember {
mutableStateOf(
TextInputState(
label = "Name",
inputConfig = InputConfig.text {
optional = true
minLength = 5
maxLength = 30
}
)
)
}For password input, use the password() InputConfig :
val passwordInput = remember {
mutableStateOf(
TextInputState(
label = "Password",
inputConfig = InputConfig.password()
)
)
}
TextInputLayout(state = passwordInput)PasswordVisualTransformation is applied by default i.e. password is unreadable. Also, visibility can be toggled using the default trailing icon :
You can hide this default trailing icon by passing showPasswordVisibilityButton as false :
TextInputLayout(
state = passwordInput,
showPasswordVisibilityButton = false
)-
For numerical inputs, we have the
number()InputConfig :val ageInput = remember { mutableStateOf( TextInputState( label = "Age", inputConfig = InputConfig.number { maxLength = 3 } ) ) } TextInputLayout(state = ageInput)
-
For decimal numbers, use the
InputConfig.decimal()InputConfig. -
For numerical inputs with fixed length, for example - contact number, we have the
fixedLengthNumber()InputConfig :val contactNoInput = remember { mutableStateOf( TextInputState( label = "Contact number", inputConfig = InputConfig.fixedLengthNumber(length = 10) ) ) } TextInputLayout(state = contactNoInput)
For email input, we have the email() InputConfig :
val emailInput = remember {
mutableStateOf(
TextInputState(
label = "Email Address",
inputConfig = InputConfig.email {
minLength = 10
}
)
)
}
TextInputLayout(state = emailInput)For a precise input validation, you can use RegexValidation :
val panNoInput = remember {
mutableStateOf(
TextInputState(
label = "PAN number",
inputConfig = InputConfig.text {
maxLength = 10
strictMaxLengthCheck = true
regexValidation = InputConfig.RegexValidation(
Regex("^[A-Z]{5}\\d{4}[A-Z]{1}$")
)
}
)
)
}
TextInputLayout(state = panNoInput)PAN number (Permanent Account Number) is a ten-character alphanumeric identifier, issued in by the Indian Income Tax Department to its citizens.
It is of the format : "{5 alphabets}{4 digits}{1 alphabet}"
You can customize the error message displayed when regex match fails :
class RegexValidation(
val regex: Regex,
val errorMessage: String? = null // Pass in the error message
)It defaults to "Invalid input!".
Supporting Text can be displayed by passing it in TextInputState constructor :
val aadharNoInput = remember {
mutableStateOf(
TextInputState(
label = "Aadhar number",
supportingText = "12 digit number",
inputConfig = InputConfig.fixedLengthNumber(12)
)
)
}The strictMaxLengthCheck property controls whether the user will be able to write characters more than the defined maxLength. If set to true, keyboard will be irresponsive after input of maxLength number of characters.
Sample :
-
maxLength set to 3
-
when true, prevents entering more than 3 characters :
-
when false, allows entering more than 3 characters but displays error message :
To check whether a single TextInputState is valid, use the hasValidInput() function :
fun MutableState<TextInputState>.hasValidInput(): BooleanExample :
Button(
onClick = {
if (nameInput.isValid()) {
val name = nameInput.value()
viewModel.registerUser(name)
}
}
) {
Text(text = "SUBMIT")
}As soon as you invoke
isValid()function, validations are performed based on theInputConfigprovided by you, including any regex validation also. Once the validation is performed, any error that exists will be displayed below the input field while highlighting the field as erroneous.
To extract the value of a TextInputState, use the value() function :
fun MutableState<TextInputState>.value(
trim: Boolean = true
): StringA long form with several TextInputLayouts can be easily validated by invoking the allHaveValidInputs() extension function :
fun TextInputState.Companion.allHaveValidInputs(
vararg states: MutableState<TextInputState>
): BooleanExample :
Button(
onClick = {
if (
TextInputState.allHaveValidInputs(
nameInput, ageInput, contactNoInput, emailInput, aadharNoInput, panNoInput
)
) {
viewModel.registerUser(
User(
name = nameInput.value(),
age = ageInput.value().toInt(),
contactNo = contactNoInput.value(),
email = emailInput.value(),
aadharNo = aadharNoInput.value(),
panNo = panNoInput.value()
)
)
}
}
) {
Text(text = "SUBMIT")
}You can also get a list of all the errors in a list of TextInputStates, by calling getErrors() function :
fun TextInputState.Companion.getErrors(
vararg states: MutableState<TextInputState>
): List<String>Complete example can be found in the sample app here.
TextInputLayout uses ImeAction.Next by-default or ImeAction.Done if you provide an doneAction lambda, which results in showing next or done button on the keyboard. It eases navigation on long forms.
You can disable this behavior by passing in imeAction as ImeAction.Default :
TextInputLayout(
state = nameInput,
imeAction = ImeAction.Default
)On the last input, you can pass the doneAction lambda which will be executed on click of keyboard's done button. It will automatically hide the keyboard also :
TextInputLayout(
state = passwordInput,
doneAction = viewModel::submitForm
)fun MutableState<TextInputState>.nullableValue(): String? // Returns null if blankfun MutableState<TextInputState>.update(newValue: String) // Can be used for prefilling the valueNote that invoking this function does not perform an pre-validations.
fun MutableState<TextInputState>.changeLabel(label: String)

