Search code examples
android-jetpackandroid-jetpack-compose

How to display error messages for multiple TextField in jetpack compose


How to display error messages for multiple TextField in jetpack compose. with only one field:

private var isError by mutableStateOf(false)

private fun validate(text: String){
    isError = if(text.isEmpty()){
        true
    }else{
        android.util.Patterns.EMAIL_ADDRESS.matcher(text).matches()
    }

    Log.i("Boolean",isError.toString())

}

    TextField(value = email,placeholder = { Text(text = "E-mail")},
            onValueChange = {
                email=it
                isError = false
            },
            shape = RoundedCornerShape(8.dp),
            colors = TextFieldDefaults.textFieldColors(
                    focusedIndicatorColor = Color.Transparent,
                    unfocusedIndicatorColor = Color.Transparent,
                    disabledIndicatorColor = Color.Transparent

            ),
            singleLine = true,
            isError = isError,
            keyboardActions = KeyboardActions { validate(email) },
            modifier=Modifier.align(Alignment.CenterHorizontally),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
            leadingIcon = { Icon(imageVector = Icons.Default.Email, contentDescription = null) })

I have a form with many TextField how do I validate one by one. for example if I have two fields with name and email. I thought about doing a loop with all the fields but I don't know if it's the best practice. Can someone help me

    var nome by rememberSaveable{ mutableStateOf("")}
    var email by rememberSaveable{ mutableStateOf("") }

       

 TextField(value = nome,placeholder = { Text(text = "Nome")},
                onValueChange = {
                    nome=it
                },
                shape = RoundedCornerShape(8.dp),
                colors = TextFieldDefaults.textFieldColors(
                        focusedIndicatorColor = Color.Transparent,
                        unfocusedIndicatorColor = Color.Transparent,
                        disabledIndicatorColor = Color.Transparent

                ),
                modifier=Modifier.align(Alignment.CenterHorizontally),
                leadingIcon = { Icon(imageVector = Icons.Default.Person, contentDescription = null) })

        Spacer(modifier = Modifier.padding(5.dp))


        TextField(value = email,placeholder = { Text(text = "E-mail")},
                onValueChange = {
                    email=it
                   
                },
                shape = RoundedCornerShape(8.dp),
                colors = TextFieldDefaults.textFieldColors(
                        focusedIndicatorColor = Color.Transparent,
                        unfocusedIndicatorColor = Color.Transparent,
                        disabledIndicatorColor = Color.Transparent

                ),
                modifier=Modifier.align(Alignment.CenterHorizontally),
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
                leadingIcon = { Icon(imageVector = Icons.Default.Email, contentDescription = null) })

    Spacer(modifier = Modifier.padding(5.dp))


    Button(
            onClick = { verifyEmpty(strings=validate) },
            colors = ButtonDefaults.buttonColors(
                    contentColor = colorResource(id = R.color.marron),
                    backgroundColor = colorResource (id = R.color.pastel_green)
            ),
    ) {
        Text(text = stringResource(id = R.string.view_cad),
                color= colorResource(id = R.color.marron))
    }

Solution

  • When you nave so much in common between two+ views, it's time to move that into a separate composable. You can specify all the differences in the parameters and not repeat same settings for each view.

    I suggest you creating state class for your custom text field. I'll store text, error text and validator logic. So you can call validate when you need: on button click or on keyboard done button:

    @Composable
    fun TestView(
    ) {
        val nomeState = rememberErrorTextFieldState("", validate = { text ->
            when {
                text.isEmpty() -> {
                    "text.isEmpty()"
                }
                else -> null
            }
        })
        val emailState = rememberErrorTextFieldState("", validate = { text ->
            when {
                text.isEmpty() -> {
                    "text.isEmpty()"
                }
                !android.util.Patterns.EMAIL_ADDRESS.matcher(text).matches() -> {
                    "pattern doesn't match"
                }
                else -> null
            }
        })
    
        Column {
            ErrorTextField(
                state = nomeState,
                placeholderText = "nome",
                leadingIconVector = Icons.Default.Person,
                modifier = Modifier.align(Alignment.CenterHorizontally),
            )
            ErrorTextField(
                state = emailState,
                placeholderText = "email",
                leadingIconVector = Icons.Default.Email,
                modifier = Modifier.align(Alignment.CenterHorizontally),
            )
            Button(
                onClick = {
                    listOf(nomeState, emailState).forEach(ErrorTextFieldState::validate)
                },
            ) {
                Text(text = "stringResource(id = R.string.view_cad)")
            }
        }
    }
    
    
    @Composable
    fun ErrorTextField(
        state: ErrorTextFieldState,
        placeholderText: String,
        leadingIconVector: ImageVector,
        modifier: Modifier,
    ) {
        Column {
            val error = state.error
            TextField(
                value = state.text,
                onValueChange = { state.updateText(it) },
                placeholder = { Text(text = placeholderText) },
                shape = RoundedCornerShape(8.dp),
                colors = TextFieldDefaults.textFieldColors(
                    focusedIndicatorColor = Color.Transparent,
                    unfocusedIndicatorColor = Color.Transparent,
                    disabledIndicatorColor = Color.Transparent,
                    errorCursorColor = Color.Red
                ),
                singleLine = true,
                isError = error != null,
                leadingIcon = { Icon(imageVector = leadingIconVector, contentDescription = null) },
                keyboardActions = KeyboardActions {
                    state.validate()
                },
                modifier = modifier,
            )
            if (error != null) {
                Text(
                    error,
                    color = Color.Red,
                )
            }
        }
    }
    
    @Composable
    fun rememberErrorTextFieldState(
        initialText: String,
        validate: (String) -> String? = { null },
    ): ErrorTextFieldState {
        return rememberSaveable(saver = ErrorTextFieldState.Saver(validate)) {
            ErrorTextFieldState(initialText, validate)
        }
    }
    
    class ErrorTextFieldState(
        initialText: String,
        private val validator: (String) -> String?,
    ) {
        var text by mutableStateOf(initialText)
            private set
    
        var error by mutableStateOf<String?>(null)
            private set
    
        fun updateText(newValue: String) {
            text = newValue
            error = null
        }
    
        fun validate() {
            error = validator(text)
        }
    
        companion object {
            fun Saver(
                validate: (String) -> String?,
            ) = androidx.compose.runtime.saveable.Saver<ErrorTextFieldState, String>(
                save = { it.text },
                restore = { ErrorTextFieldState(it, validate) }
            )
        }
    }