Текстовое поле для создания 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 ответ
Я обнаружил следующие проблемы с исходным кодом:
- Сами поля формы никогда не запоминались (поэтому вложенные свойства состояния заменялись новыми при каждом рендеринге)
- The
Required
класс имел ссылочную семантику, поэтомуfield.validators.contains(Required())
никогда бы не совпало - Вычисление
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")
}
}
}
исправил проблему.