Skip to content

Latest commit

 

History

History
410 lines (301 loc) · 9.93 KB

TextInputLayout.md

File metadata and controls

410 lines (301 loc) · 9.93 KB

TextInputLayout

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

Basic usage

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 label is a part of state. Moving ahead you will find more such OutlinedTextField's parameters as a property of TextInputState class.

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"
        )
    }
)

TextInputState & InputConfig

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(): InputConfig

Which 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
}

Customizing InputConfig

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
            }
        )
    )
}

Password input

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
)

Numerical input

  • 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)

Email input

For email input, we have the email() InputConfig :

val emailInput = remember {
    mutableStateOf(
        TextInputState(
            label = "Email Address",
            inputConfig = InputConfig.email {
                minLength = 10
            }
        )
    )
}

TextInputLayout(state = emailInput)

RegexValidation

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

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)
        )
    )
}

strictMaxLengthCheck

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 :


Validation

Single state validation

To check whether a single TextInputState is valid, use the hasValidInput() function :

fun MutableState<TextInputState>.hasValidInput(): Boolean

Example :

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 the InputConfig provided 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.

Extracting value

To extract the value of a TextInputState, use the value() function :

fun MutableState<TextInputState>.value(
    trim: Boolean = true
): String

Form / Multiple state validation

A long form with several TextInputLayouts can be easily validated by invoking the allHaveValidInputs() extension function :

fun TextInputState.Companion.allHaveValidInputs(
    vararg states: MutableState<TextInputState>
): Boolean

Example :

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

Complete example can be found in the sample app here.


Default IME Actions

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
)

Other helpful functions

getNullableValue()

fun MutableState<TextInputState>.nullableValue(): String? // Returns null if blank

updateValue()

fun MutableState<TextInputState>.update(newValue: String) // Can be used for prefilling the value

Note that invoking this function does not perform an pre-validations.

changeLabel()

fun MutableState<TextInputState>.changeLabel(label: String)