kotlin 读取非编写状态时清除Jetpack编写文本字段

flvtvl50  于 2023-06-24  发布在  Kotlin
关注(0)|答案(2)|浏览(129)

我有一个Jetpack compose表单,从this tutorial创建。它通过在您单击提交按钮时验证表单来很好地显示错误。我希望表单上的按钮能够使用相同的表单状态动态启用。
问题是state.requiredFieldsNotEmpty()似乎在每次调用可变状态文本变量时都会重置它。这意味着打字什么也不做,因为它立即清除。为什么?它所做的只是读取状态变量,没有写入。专门调用这一行field.text.length < it.length会触发bug并清除field.text。
相关编码:

val state by remember {
            mutableStateOf(FormState())
        }

        Column {
            Form(
                state = state,
                fields = listOf(
                    Field(name = "pan",
                        placeholderHint = R.string.card_number,
                        validators = listOf(Required(), MinLength(length = Constants.MINIMUM_PAN_LENGTH))),
                )
            )

            Row {
                TextButton(
                    onClick = { if (state.validate()) submitData(state.getData()) },
                    enabled = state.requiredFieldsNotEmpty(),
                ) {
                    Text(
                        text = stringResource(id = R.string.confirm),
                    )
                }

            }
        }

Form.kt:

@Composable
fun Form(
    modifier: Modifier,
    state: FormState,
    fields: List<Field>) {
    state.fields = fields

    Column(modifier = modifier.padding(horizontal = 16.dp)) {
        fields.forEach {
            it.Content(modifier)
        }
    }
}

class FormState {
    var fields: List<Field> = listOf()
        set(value) {
            field = value
        }

    fun validate(): Boolean {
        var valid = true
        for (field in fields) if (!field.validate()) {
            valid = false
            break
        }
        return valid
    }

    fun requiredFieldsNotEmpty(): Boolean {
        for (field in fields){
            if(field.validators.contains(Required()) && field.text.isBlank()){
                return false
            }
            val minLength = field.validators.find { it is MinLength } as MinLength?
            minLength?.let {
                if(field.text.length < it.length){
                    return false
                }
            }
        }
        return true
    }

    fun getData(): Map<String, String> = fields.map { it.name to it.text }.toMap()
}

Validators.kt

sealed interface Validator
open class Email(var message: Int = emailMessage) : Validator
open class NotExpired(var message: Int = expiryMessage) : Validator
open class Required(var message: Int = requiredMessage) : Validator
open class MinLength(val length: Int, var message: Int = minLengthMessage): Validator

Field.kt

class Field(
    val name: String,
    val placeholderHint: Int = R.string.empty,
    val error: Int = R.string.empty,
    val singleLine: Boolean = true,
    val keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
    val validators: List<Validator>
) {
    var text: String by mutableStateOf("")
    var supportingError: Int by mutableStateOf(error)
    var minLength: Int by mutableStateOf(-1)
    var hasError: Boolean by mutableStateOf(false)

    private fun showError(error: Int) {
        hasError = true
        supportingError = error
    }

    private fun showMinLengthError(error: Int, length: Int) {
        hasError = true
        minLength = length
        supportingError = error
    }

    private fun hideError() {
        supportingError = error
        minLength = -1
        hasError = false
    }

    @Composable
    fun Content(
        modifier: Modifier,
    ) {
        OutlinedTextField(value = text, isError = hasError, supportingText = {
            Text(text = stringResource(id = supportingError))
        }, singleLine = singleLine, keyboardOptions = keyboardOptions, placeholder = {
            Text(
                text = stringResource(id = placeholderHint),
            )
        }, modifier = modifier
            .fillMaxWidth()
            .padding(10.dp), onValueChange = { value ->
            hideError()
            text = value
        })
    }

    fun validate(): Boolean {
        return validators.map {
            when (it) {
                is Email -> {
                    if (!Patterns.EMAIL_ADDRESS.matcher(text).matches()) {
                        showError(it.message)
                        return@map false
                    }
                    true
                }

                is Required -> {
                    if (text.isEmpty()) {
                        showError(it.message)
                        return@map false
                    }
                    true
                }

                is NotExpired -> {
                    val month = text.substring(0, 2).trimStart('0').toInt()
                    val year = text.substring(2, 4).trimStart('0').toInt()
                    val now = LocalDate.now()
                    if ((year < now.year) || (year == now.year && month < now.monthValue)) {
                        showError(it.message)
                        return@map false
                    }
                    true
                }

                is MinLength -> {
                    if (text.length < it.length) {
                        showMinLengthError(it.message, it.length)
                        return@map false
                    }
                    true
                }
            }
        }.all { it }
    }
}
erhoui1w

erhoui1w1#

我发现原始代码存在以下问题:
1.表单字段本身永远不会被记住(因此嵌套的状态属性将在每次呈现时被新属性替换)

  1. Required类具有引用语义,因此field.validators.contains(Required())永远不会匹配
  2. requiredFieldsNotEmpty()的计算依赖于状态(由于声明var text: String by mutableStateOf("")),但没有 Package 在derivedStateOf
    以下调整:
sealed interface Validator
open class Email(var message: String = EMAIL_MESSAGE): Validator
data class Required(var message: String = REQUIRED_MESSAGE): Validator
open class Regex(var message: String, var regex: String = REGEX_MESSAGE): Validator
open class MinLength(val length: Int, var message: String = MIN_LENGTH_MESSAGE): Validator
@Preview
    @Composable
    fun Screen(){
        val fields = remember {
            listOf(
                Field(name = "username", validators = listOf(Required(), MinLength(length = 3))),
                Field(name = "email", validators = listOf(Required(), Email()))
            )
        }

        val state by remember { mutableStateOf(FormState()) }
        val buttonEnabled by remember { derivedStateOf { state.requiredFieldsNotEmpty() } }

        Column {
            Form(
                state = state,
                fields = fields
            )
            Button(
                enabled = buttonEnabled,
                onClick = { if (state.validate()) toast("Our form works!") }) {
                Text("Submit")
            }
        }
    }

解决了这个问题。

oalqel3c

oalqel3c2#

我尝试了提供的代码,我发现从UI中提取状态是最佳实践。
在提供的代码中,FormComposable函数遵循了从UI中提取状态的最佳实践。Form类负责维护表单字段的状态,并处理表单字段的验证。UI是使用JetpackCompose可组合函数组合的,这些函数使用Form类提供的状态。

class FormFieldState(
    val name: String,
    label: String,
    placeHolder: String? = null,
    val keyboardType: KeyboardType = KeyboardType.Text,
    private val validators: List<Validator> = emptyList()
) {
    var text by mutableStateOf("")
        private set
 
    var label by mutableStateOf(label)
        private set

    var supportingText by mutableStateOf("")
        private set

    val placeHolderText by mutableStateOf(placeHolder)
    var isError by mutableStateOf(false)
        private set

    fun onValueChange(value: String) {
        text = value
        isError = false
        supportingText = ""
    }

    @RequiresApi(Build.VERSION_CODES.O)
    fun validate(): Boolean {
        return validators.map {
            val isValid = when (it) {
                is Email -> Patterns.EMAIL_ADDRESS.matcher(text).matches()
                    // Validates if the text matches a valid email address pattern

                is Required -> text.isNotEmpty()
                    // Validates if the text is not empty (i.e., required)

                is NotExpired -> {
                    val month = text.substring(0, 2).trimStart('0').toInt()
                    val year = text.substring(2, 4).trimStart('0').toInt()
                    val now = LocalDate.now()
                    (year < now.year) || (year == now.year && month < now.monthValue)
                }
                    // Validates if the date is not expired by comparing the month and year

                is MinLength -> text.length > it.length
                    // Validates if the text has a minimum length

            }
            if (!isValid) {
                supportingText = it.message
                isError = true
                // If the validation fails, set the supporting text and mark the field as an error
            }
            isValid
            // Returns the result of the validation
        }.all { it }
        // Returns true if all validations pass, false otherwise
    }
}

FormField函数是一个可组合的函数,用于表示用户界面中的表单字段。以FormFieldState类型的formFieldState对象和onValueChange回调函数为参数

@Composable
fun FormField(formFieldState: FormFieldState, onValueChange: (String) -> Unit) {
    OutlinedTextField(
        value = formFieldState.text,
        onValueChange = onValueChange,
        label = { Text(formFieldState.label) },
        supportingText = { Text(formFieldState.supportingText) },
        isError = formFieldState.isError,
        keyboardOptions = KeyboardOptions(keyboardType = formFieldState.keyboardType),
        placeholder = { formFieldState.placeHolderText?.let { Text(it) } }
    )
}

Form类表示表单字段的集合,提供处理和验证表单数据的功能。

class Form(val fields: List<FormFieldState>) {

    // State Indicates whether any form fields are empty
    var hasEmptyFields by mutableStateOf(true)
        private set

    // Handles the value change of a form field
    // Updates the form field's value and checks if any fields are empty
    fun onValueChange(fieldState: FormFieldState, value: String) {
        fieldState.onValueChange(value)
        hasEmptyFields = value.isBlank()
    }

    // Validates all the form fields
    @RequiresApi(Build.VERSION_CODES.O)
    fun validate() = fields.all { it.validate() }

    // Retrieves the form data as a map
    fun formData() = fields.associate { it.name to it.text }
}

FormFormFieldState的一个示例用法。

@RequiresApi(Build.VERSION_CODES.O)
@Preview
@Composable
fun FormComposable(submitData: (Map<String, String>) -> Unit = {}) {
    val form = remember {
        Form(
            listOf(
                FormFieldState("firstName", "First name", validators = listOf(Required())),
                FormFieldState("lastName", "Last name", validators = listOf(Required())),
                FormFieldState(
                    "panNumber",
                    "Pan number",
                    validators = listOf(MinLength(8), Required()),
                    keyboardType = KeyboardType.Number
                ),
            )
        )
    }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        form.fields.forEach {
            FormField(it) { value ->
                form.onValueChange(it, value)
            }
        }

        Button(
            modifier = Modifier.align(Alignment.CenterHorizontally),
            enabled = !form.hasEmptyFields,
            onClick = { if (form.validate()) submitData(form.formData()) }
        ) {
            Text(text = "Submit")
        }
    }
}

相关问题