OPT TextField in Android Jetpack Compose (No need of Library)
You are here, that means you are trying to use OTP verification in you app. Yes you saw that right, you don’t need a heavy library to make a functional OTP input UI. I’ll guide you step by step to get the desired UI which you’re completely free to modify it further.
Edit: Add copied OTP code at once. Make documentation for more contrast. Leverage ViewModel for update.
Let’s get started ;)
There are many way to do it, but I will discuss two, and give you solution of one. Of course, I will give you the best solution known to me.
- Using BasicTextField.
Here, we use the BasicTextField’s decorationBox for decorating the text by character either using border, or any other composable like Box for line like underlined text, or any other custom way. You can check this article for this.
2. Using OutlinedTextField
In this method, we use multiple OutlinedTextFields in a Row and manage their states and values accordingly. We use FocusRequester() to manage focus state, and use array to store the values of each text fields which will be updated by demand or in flow.
Yes, I do have thought of the focus change in Backspace and on new value input.
Since, we are using OutlinedTextField, the error handling, enabled and other visual properties are easily manageable.
val otpValues = remember { mutableStateListOf(*Array(otpLength) { "" }) } // Not good practice, use viewModel
val focusRequesters = List(otpLength) { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
- otpValues — stores the OTP numbers which can be of any length as otpLength. The spread operator (*) is used to convert the array into a list so that they can be passed as individual arguments. We can understand it as list of individual strings.
- focusRequesters — list of focus requesters for each text fields for each otpValues.
- keyboardController — to manage the state of ime keyboard i.e, show or hide.
Row(
modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
otpValues.forEachIndexed { index, value ->
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequesters[index])
.onKeyEvent { keyEvent ->
if (keyEvent.key == Key.Backspace) {
if (otpValues[index].isEmpty() && index > 0) {
otpValues[index] = ""
focusRequesters[index - 1].requestFocus()
} else {
otpValues[index] = ""
}
true
} else {
false
}
},
value = value,
onValueChange = { newValue ->
if (newValue.length <= 1) {
otpValues[index] = newValue
if (newValue.isNotEmpty()) {
if (index < otpLength - 1) {
focusRequesters[index + 1].requestFocus()
} else {
keyboardController?.hide()
}
}
} else {
if (index < otpLength - 1) focusRequesters[index + 1].requestFocus()
}
},
... }
LaunchedEffect(Unit) {
focusRequesters.first().requestFocus()
}
Here, we have a Row which has the otpValues size of OutlinedTextField i.e. otpLength no of text fields (as declared before).
For each text field we have a focus requester set which updates accordingly with change on keyEvent, and on value change in text field.
Let’s understand the logic in short,
- onKeyEvent — if backSpace is pressed then we check if the value on the focused text field is empty or not. If the value is empty we change the focus to previous i.e. left side text field if exits. If its not empty then we simply clear the text field.
- onValueChange — if the new value is the text field is less or equals to 1 we assign the new value i.e, it can be empty or a number. If the value is updated with non empty string then we shift the focus to the next text field if it exists, else we are at the last text field so we hide the keyboard.
The last code i.e. inside the LauncedEffect helps us to focus the first text field when the UI is drawn.
It’s all about the focus management and value updation in each text field. You can track each focusRequester to understand the logic behind this.
Edit: You can find the previous code here. The below represents latest code after
some changes (which I encourage to do using viewModel).
I am thoughtful about the function so I have added documentation in the new code to help you understand it more clearly. I am trying to make it better every time I use which I will be updating with time. ;)
Note: If you feel stuck with the below code that prefers viewModel, you can compare the old code and easily understand whats going on. (If confused, don’t forget to check the revisions in Gist.)
This is one of the thousands way of doing this. And be sure that, there’s always better way to do it!
Thank you for visiting! Keep exploring!! ;)