Blog Infos
Author
Published
Topics
, ,
Published

This post will not only focus on manual & auto validation of TextFieldinputs, but also on how to provide a nice UX by handling process death, including highlighting the last focused field upon restoration.

This post will not only focus on manual & auto validation of TextField inputs, but also on how to provide a nice UX by handling process death, including highlighting the last focused field upon restoration.

Auto-validation sample

Setup
Part 1: InputWrapper
@Parcelize
data class InputWrapper(
val value: String = "",
val errorId: Int? = null
) : Parcelable
view raw InputWrapper.kt hosted with ❤ by GitHub
Part 2: ViewModel (simplified version)
@HiltViewModel
class InputValidationAutoViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val name = handle.getStateFlow(NAME, InputWrapper())
val creditCardNumber = handle.getStateFlow(CREDIT_CARD_NUMBER, InputWrapper())
val areInputsValid = combine(name, creditCardNumber) { name, cardNumber ->
name.value.isNotEmpty() && name.errorId == null && cardNumber.value.isNotEmpty() && cardNumber.errorId == null
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), false)
private val _events = Channel<ScreenEvent>()
val events = _events.receiveAsFlow()
fun onNameEntered(input: String) {
val errorId = InputValidator.getNameErrorIdOrNull(input)
handle[NAME] = name.value.copy(value = input, errorId = errorId)
}
fun onCardNumberEntered(input: String) {
val errorId = InputValidator.getCardNumberErrorIdOrNull(input)
handle[CREDIT_CARD_NUMBER] = creditCardNumber.value.copy(value = input, errorId = errorId)
}
fun onContinueClick() {
val resId = if (areInputsValid.value) R.string.success else R.string.validation_error
_events.trySend(ScreenEvent.ShowToast(resId))
}
}
handle[NAME] = name.value.copy(value = input, errorId = errorId)
view raw emitValue.kt hosted with ❤ by GitHub
Part 3: Composables (simplified version)
@Composable
fun InputValidationScreen(viewModel: InputValidationAutoViewModel = hiltViewModel()) {
val events = remember(viewModel.events, lifecycleOwner) {..}
val name by viewModel.name.collectAsStateWithLifecycle()
val creditCardNumber by viewModel.creditCardNumber.collectAsStateWithLifecycle()
val areInputsValid by viewModel.areInputsValid.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
is ScreenEvent.ShowToast -> context.toast(event.messageId)
}
}
}
Column {
CustomTextField(
inputWrapper = name,
onValueChange = viewModel::onNameEntered
)
CustomTextField(
inputWrapper = creditCardNumber,
onValueChange = viewModel::onCardNumberEntered,
onImeKeyAction = viewModel::onContinueClick
)
Button(onClick = viewModel::onContinueClick, enabled = areInputsValid) {
Text(text = "Continue")
}
}
}
@Composable
fun CustomTextField(
inputWrapper: InputWrapper,
onValueChange: OnValueChange,
onImeKeyAction: OnImeKeyAction
) {
Column {
TextField(
value = inputWrapper.value,
onValueChange = { onValueChange(it) },
isError = inputWrapper.errorId != null,
keyboardActions = KeyboardActions(onAny = { onImeKeyAction() }),
)
if (inputWrapper.errorId != null) {
Text() // error message
}
}
}
Enhancements
Part 1: ViewModel
enum class FocusedTextFieldKey {
NAME, CREDIT_CARD_NUMBER, NONE
}
@HiltViewModel
class InputValidationViewModel @Inject constructor(..) : ViewModel() {
private var focusedTextField = handle.get("focusedTextField") ?: FocusedTextFieldKey.NAME
set(value) {
field = value
handle.set("focusedTextField", value)
}
init {
if (focusedTextField != FocusedTextFieldKey.NONE) focusOnLastSelectedTextField()
}
fun onTextFieldFocusChanged(key: FocusedTextFieldKey, isFocused: Boolean) {
focusedTextField = if (isFocused) key else FocusedTextFieldKey.NONE
}
fun onNameImeActionClick() {
_events.trySend(ScreenEvent.MoveFocus(FocusDirection.Down))
}
fun onContinueClick() {
viewModelScope.launch(Dispatchers.Default) {
if (areInputsValid.value) clearFocusAndHideKeyboard()
..
}
}
private suspend fun clearFocusAndHideKeyboard() {
_events.send(ScreenEvent.ClearFocus)
_events.send(ScreenEvent.UpdateKeyboard(false))
focusedTextField = FocusedTextFieldKey.NONE
}
private fun focusOnLastSelectedTextField() {
viewModelScope.launch(Dispatchers.Default) {
_events.send(ScreenEvent.RequestFocus(focusedTextField))
delay(250)
_events.send(ScreenEvent.UpdateKeyboard(true))
}
}
}

Job Offers

Job Offers


    Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Mobile Engineer

    OLX Group
    Remote, Portugal, Spain, Romania, Poland
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

,

At long last we have Kotlin first at Meta!

Android started to support Kotlin 5 years ago and became the first-choice language three years ago. But Meta just announced Kotlin as the preferred and default language for our Android code base only 3 months…
Watch Video

At long last we have Kotlin first at Meta!

Peng Jiang & Sergei Rybalkin
Software Engineer & Kotlin
Meta

At long last we have Kotlin first at Meta!

Peng Jiang & Serge ...
Software Engineer & ...
Meta

At long last we have Kotlin first at Meta!

Peng Jiang & Ser ...
Software Engineer & Kotli ...
Meta

Jobs

Part 2: Composables
@Composable
fun InputValidationScreen(..) {
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
val creditCardNumberFocusRequester = remember { FocusRequester() }
val nameFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
is ScreenEvent.UpdateKeyboard -> {
if (event.show) keyboardController?.show() else keyboardController?.hide()
}
is ScreenEvent.ClearFocus -> focusManager.clearFocus()
is ScreenEvent.RequestFocus -> {
when (event.textFieldKey) {
FocusedTextFieldKey.NAME -> nameFocusRequester.requestFocus()
FocusedTextFieldKey.CREDIT_CARD_NUMBER -> creditCardNumberFocusRequester.requestFocus()
}
}
is ScreenEvent.MoveFocus -> focusManager.moveFocus(event.direction)
}
}
}
Column {
CustomTextField(
modifier = Modifier
.focusRequester(nameFocusRequester)
.onFocusChanged { focusState ->
viewModel.onTextFieldFocusChanged(
key = FocusedTextFieldKey.NAME,
isFocused = focusState.isFocused
)
},
onImeKeyAction = viewModel::onNameImeActionClick
)
CustomTextField(
modifier = Modifier
.focusRequester(creditCardNumberFocusRequester)
.onFocusChanged { focusState ->
viewModel.onTextFieldFocusChanged(
key = FocusedTextFieldKey.CREDIT_CARD_NUMBER,
isFocused = focusState.isFocused
)
}
)
}
}
@Composable
fun CustomTextField(
modifier: Modifier,
inputWrapper: InputWrapper,
onValueChange: OnValueChange
) {
val fieldValue = remember {
mutableStateOf(TextFieldValue(inputWrapper.value, TextRange(inputWrapper.value.length)))
}
Column {
TextField(
modifier = modifier,
value = fieldValue.value,
onValueChange = {
fieldValue.value = it
onValueChange(it.text)
}
)
..
}
}
Manual Validation
private class InputErrors(
val nameErrorId: Int?,
val cardErrorId: Int?
)
@HiltViewModel
class FormValidationViewModel @Inject constructor(..) : ViewModel() {
fun onNameEntered(input: String) {
handle[NAME] = name.value.copy(value = input, errorId = null)
}
fun onContinueClick() {
viewModelScope.launch(Dispatchers.Default) {
when (val inputErrors = getInputErrorsOrNull()) {
null -> {
clearFocusAndHideKeyboard()
_events.send(ScreenEvent.ShowToast(R.string.success))
}
else -> displayInputErrors(inputErrors)
}
}
}
private fun getInputErrorsOrNull(): InputErrors? {
val nameWrapper = name.value
val cardWrapper = creditCardNumber.value
val nameErrorId = InputValidator.getNameErrorIdOrNull(nameWrapper.value)
val cardErrorId = InputValidator.getCardNumberErrorIdOrNull(cardWrapper.value)
return if (nameErrorId == null && cardErrorId == null) {
null
} else {
InputErrors(nameErrorId, cardErrorId)
}
}
private suspend fun displayInputErrors(inputErrors: InputErrors) {
handle[NAME] = name.value.copy(errorId = inputErrors.nameErrorId)
handle[CREDIT_CARD_NUMBER] = creditCardNumber.value.copy(errorId = inputErrors.cardErrorId)
}
}

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu