Текстовое поле для создания Jetpack очищается при чтении состояния отсутствия создания

У меня есть форма создания Jetpack, созданная на основе этого руководства . Он хорошо отображает ошибки, проверяя форму при нажатии кнопки отправки. Я хочу, чтобы кнопка в моей форме была динамически включена, используя то же состояние формы.

Проблема в том, чтоstate.requiredFieldsNotEmpty()похоже, сбрасывает текстовую переменную изменяемого состояния при каждом ее вызове. Значение ввода ничего не дает, потому что оно мгновенно очищается. Почему? Все, что он делает, это читает переменную состояния, а не записывает. Конкретно вызывая эту строкуfield.text.length < it.lengthвызывает ошибку и очищает 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),
                    )
                }

            }
        }

Форма.кт:

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

Валидаторы.кт

      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

Поле.кт

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

1 ответ

Я обнаружил следующие проблемы с исходным кодом:

  1. Сами поля формы никогда не запоминались (поэтому вложенные свойства состояния заменялись новыми при каждом рендеринге)
  2. The Requiredкласс имел ссылочную семантику, поэтомуfield.validators.contains(Required())никогда бы не совпало
  3. ВычислениеrequiredFieldsNotEmpty()зависело от состояния (из-за декларацииvar text: String by mutableStateOf("")), но не был завернут в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")
            }
        }
    }

исправил проблему.