最佳方式记住具有多个部分的表单的状态

huangapple go评论70阅读模式
英文:

Best way of remembering the state of a form with multiple sections

问题

我理解你的问题。以下是你提供的内容的翻译:

"我正在开发的应用将具有一个表单屏幕,该屏幕可能具有多个“TextArea”字段、复选框、下拉框等。以下是一个这样的表单的示例:

最佳方式记住具有多个部分的表单的状态

现在我想要以某种方式记住每个字段的状态,以便能够在导航操作中保持它(类似于rememberSaveable),然后将所有数据转换为单个JSON,然后发送到后端。值得注意的是,底部的按钮只有在每个部分都有数据填写时才能启用,即文本区域不为空,至少选择了一个复选框等。

在避免为每个组件使用rememberSaveable字段的情况下,最好的方法是什么?我应该使用一个Form数据类,用于存储下拉框组件名称和答案的Map<String, String>,以及对其他部分采用类似的方式吗?

此外,有没有办法避免在每个“onValueChanged”回调中检查所有表单部分是否存在数据以启用按钮?

这是屏幕的当前状态:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RegistrationStep2Screen(
    navController: NavController,
    registrationViewModel: RegistrationViewModel = getViewModel()
) {
    // ...(省略了其他部分)
}

FormCheckbox组合框:

@Composable
fun FormCheckBox(
    modifier: Modifier = Modifier,
    label: String = "",
    isCheckedInitVal: Boolean = false,
    onCheckedStatusChange: (Boolean) -> Unit
) {
    // ...(省略了其他部分)
}

ViewModel:

class RegistrationViewModel(
    private val registrationRepository: RegistrationRepository
) : ViewModel() {
    // ...(省略了其他部分)
}

在发布之前,我实际上尝试过使用一个单独的表单数据类,就像我提到的那样,并在每个组件的回调中更新它,但似乎实际上并没有记住状态,尽管它存储在ViewModel中并通过LiveData使用observeAsState进行观察。"

希望这对你有帮助。如果你有其他问题,请随时提问。

英文:

The app I'm building will have a form screen that could have multiple "TextArea" fields, checkboxes, dropdowns, etc. Here's an example of such a form:

最佳方式记住具有多个部分的表单的状态

Now I want to somehow remember the state of each field in order to be able to persist it across navigation actions (similarly to rememberSaveable) and then convert all the data into a single JSON that I will then send to the backend. It's worth noting that the button at the bottom should only be enabled if every section has data filled, i.e. text area is not empty, at least one checkbox is selected, etc.

What would be the best way to remember all the info while avoiding having a rememberSaveable field per component? Should I use a Form data class that will hold a Map<String,String> for storing dropdown component name & answers for example and similarly for the rest of the sections?

Also, is there any way I can avoid checking if all form sections have data present in order to enable the button, in each and every "onValueChanged" callback?

Here's the screen right now:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RegistrationStep2Screen(
    navController: NavController,
    registrationViewModel: RegistrationViewModel = getViewModel()
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(vertical = 40.dp, horizontal = 16.dp)
            .verticalScroll(rememberScrollState())
    ) {
        var section1Text by rememberSaveable { mutableStateOf("") }
        var selectedDropdownOption by rememberSaveable { mutableStateOf("Answer") }
        val checkboxStates by registrationViewModel.checkboxStates.observeAsState()
        val canProceed by registrationViewModel.canProceedToStep3.observeAsState(false)

        LaunchedEffect(
            key1 = section1Text,
            key2 = checkboxStates!!.values.count { it },
            key3 = selectedDropdownOption
        ) {
            registrationViewModel.canProceedToStep3(
                section1Text,
                checkboxStates!!.values.count { it },
                selectedDropdownOption
            )
        }

        RegistrationProgressBar(currentStep = 2)

        Spacer(modifier = Modifier.height(30.dp))

        Column(
            Modifier
                .fillMaxWidth()
        ) {
            Text(
                text = "STEP 2",
                style = MaterialTheme.typography.displayMedium,
                fontSize = 16.sp,
                color = colorResource(id = R.color.form_field_error_color),
                fontFamily = FontFamily(Font(R.font.montserrat_bold)),
                fontWeight = FontWeight.Bold
            )
            Text(
                text = "Please answer the following questions",
                style = MaterialTheme.typography.bodySmall
            )
        }


        Spacer(modifier = Modifier.height(20.dp))

        Column(
            modifier = Modifier
                .fillMaxWidth(),
            verticalArrangement = Arrangement.spacedBy(20.dp),
            horizontalAlignment = Alignment.Start
        ) {
            FormSection(
                orderNum = 1,
                title = "Question full text"
            ) {
                OutlinedTextField(
                    modifier = Modifier
                        .padding(top = 10.dp)
                        .fillMaxWidth()
                        .heightIn(min = 111.dp, max = 111.dp),
                    value = section1Text,
                    onValueChange = { value ->
                        section1Text = value.trim()
                    },
                    colors = TextFieldDefaults.outlinedTextFieldColors(
                        unfocusedBorderColor = colorResource(id = R.color.form_field_border_color),
                        focusedBorderColor = colorResource(id = R.color.form_field_border_color),
                        errorBorderColor = colorResource(id = R.color.form_field_error_color),
                        errorSupportingTextColor = colorResource(id = R.color.form_field_error_color)
                    ),
                )
            }

            FormSection(
                orderNum = 2,
                title = "Question multiple choice"
            ) {
                Row(
                    modifier = Modifier.padding(top = 13.dp),
                    horizontalArrangement = Arrangement.spacedBy(40.dp)
                ) {
                    Column(verticalArrangement = Arrangement.spacedBy(11.dp)) {
                        FormCheckBox(
                            onCheckedStatusChange = {
                                registrationViewModel.updateCheckboxState(0 to it)
                            },
                            isCheckedInitVal = checkboxStates!![0]!!,
                            label = "Answer 1"
                        )
                        FormCheckBox(
                            onCheckedStatusChange = { registrationViewModel.updateCheckboxState(1 to it) },
                            isCheckedInitVal = checkboxStates!![1]!!,
                            label = "Answer 2"
                        )
                    }

                    Column(verticalArrangement = Arrangement.spacedBy(11.dp)) {
                        FormCheckBox(
                            onCheckedStatusChange = { registrationViewModel.updateCheckboxState(2 to it) },
                            isCheckedInitVal = checkboxStates!![2]!!,
                            label = "Answer 3"
                        )
                        FormCheckBox(
                            onCheckedStatusChange = { registrationViewModel.updateCheckboxState(3 to it) },
                            isCheckedInitVal = checkboxStates!![3]!!,
                            label = "Answer 4"
                        )
                    }
                }
            }

            FormSection(
                orderNum = 3,
                title = "Question drop down"
            ) {
                FormDropdownField(
                    modifier = Modifier
                        .fillMaxWidth(),
                    options = listOf("Answer 1", "Answer 2", "Answer 3"),
                    defaultOption = selectedDropdownOption
                ) {
                    selectedDropdownOption = it
                }
            }
        }
        Spacer(Modifier.weight(1f))

        if (!canProceed) {
            Text(
                text = "Please complete all fields to proceed to the next step",
                style = MaterialTheme.typography.bodyMedium,
                fontSize = 14.sp,
                color = colorResource(id = R.color.form_field_error_color)
            )

            Spacer(Modifier.height(10.dp))
        }

        FoodakaiButton(
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp),
            text = stringResource(id = R.string.next_step),
            enabled = canProceed,
            onClick = {
                navController.navigate(Screens.REGISTRATION_STEP3.navRoute)
            }
        )
    }
}

FormCheckbox composable:

@Composable
fun FormCheckBox(
    modifier: Modifier = Modifier,
    label: String = "",
    isCheckedInitVal: Boolean = false,
    onCheckedStatusChange: (Boolean) -> Unit
) {
    var isChecked by rememberSaveable { mutableStateOf(isCheckedInitVal) }

    Row(verticalAlignment = Alignment.CenterVertically) {
        Box(
            contentAlignment = Alignment.Center,
            modifier = modifier
                .width(22.dp)
                .height(22.dp)
                .border(
                    2.dp,
                    colorResource(id = R.color.form_field_border_color),
                    MaterialTheme.shapes.medium
                )
                .padding(1.dp)
                .background(
                    colorResource(id = if (isChecked) R.color.form_checkbox_filled_color else R.color.white),
                    MaterialTheme.shapes.medium
                )
                .clip(MaterialTheme.shapes.medium)
                .clickable {
                    isChecked = !isChecked
                    onCheckedStatusChange(isChecked)
                }
        ) {
        }
        Spacer(modifier = Modifier.width(16.dp))
        Text(
            label,
            fontSize = 16.sp,
            fontFamily = FontFamily(listOf(Font(R.font.montserrat_regular)))
        )
    }
}

viewmodel:

class RegistrationViewModel(
    private val registrationRepository: RegistrationRepository
) : ViewModel() {
    private var _checkboxStates = MutableLiveData(
        mapOf(
            0 to false,
            1 to false,
            2 to false,
            3 to false
        )
    )
    val checkboxStates: LiveData<Map<Int, Boolean>> = _checkboxStates

    private var _userData = MutableLiveData<RegistrationCheckResponse>(null)
    val userData: LiveData<RegistrationCheckResponse>
        get() = _userData

    private var _canProceedToStep3 = MutableLiveData(false)
    val canProceedToStep3: LiveData<Boolean>
        get() = _canProceedToStep3

    fun updateCheckboxState(state: Pair<Int, Boolean>) {
        val map = _checkboxStates.value?.toMutableMap()
        map?.apply {
            map[state.first] = state.second
            _checkboxStates.value = map.toMap()
        }
    }

    fun canProceedToStep2(email: String, confirmEmail: String) =
        areEmailsMatching(email, confirmEmail) && isEmailValid(email)

    fun areEmailsMatching(email: String, confirmEmail: String) =
        email.trim() == confirmEmail.trim()

    fun isEmailValid(email: String) =
        InputValidator.validateEmail(email)

    fun canProceedToStep3(
        checkboxText: String,
        checkboxCount: Int,
        selectedDropdownValue: String
    ) {
        _canProceedToStep3.value =
            checkboxText.isNotBlank() && checkboxCount > 0 && selectedDropdownValue.isNotEmpty()
    }

    fun verifyByEmail(email: String, onResponse: (Boolean) -> Unit) {
        viewModelScope.launch(Dispatchers.IO) {
            when (val response = registrationRepository.isRegisteredByEmail(email)) {
                is ResponseResult.Success -> {
                    response.data?.let {
                        withContext(Dispatchers.Main) {
                            onResponse(it.isVerified)
                        }
                    }
                }

                is ResponseResult.Error -> {
                    withContext(Dispatchers.Main) {
                        onResponse(false)
                    }
                }
            }
        }
    }

    fun verifyByRegistrationId(regId: String) {
        viewModelScope.launch(Dispatchers.IO) {
            when (val response = registrationRepository.isRegisteredById(regId)) {
                is ResponseResult.Success -> {
                    response.data?.let {
                        _userData.postValue(it)
                    }
                }

                is ResponseResult.Error -> {
                    println(response.exception.message)
                }
            }
        }
    }

    fun resendVerificationEmail() {
        viewModelScope.launch(Dispatchers.IO) {
            val response = _userData.value?.id?.let {
                registrationRepository.resendVerificationRequest(it)
            }

            when (response) {
                is ResponseResult.Success -> {
                    println("SUCCESS")
                }

                is ResponseResult.Error -> {
                    println("Exception: ${response.exception.message}")
                }

                else -> {
                    println("UNKNOWN ERROR")
                }
            }
        }
    }

    fun verifyUser(onSuccess: () -> Unit, onFailure: () -> Unit) {
        viewModelScope.launch(Dispatchers.IO) {
            when (val response = _userData.value?.id?.let {
                registrationRepository.verifyUser(it)
            }) {
                is ResponseResult.Success -> {
                    onSuccess()
                }

                else -> {
                    onFailure()
                }
            }
        }

    }
}

Before posting here I actually tried using a single Form data class like I mentioned and updating it with each component's callback but it doesn't seem to actually remember the state even though it was being stored in the viewModel and observed through LiveData using observeAsState.

答案1

得分: 1

没有适当的方式,除非使用Jetpack Room库。我遇到了类似的问题(记住将来重用后端的状态),并意识到即使在重新启动后,我也应该能够恢复程序状态。是的,使用Room听起来有点多余,但这是唯一的方法。您也可以使用共享首选项或数据存储,但如果要实现高度结构化的表单,应该以高度结构化的方式保存数据。

英文:

There is no proper way except for using Jetpack Room library. I was stuck with similar problem (remembering state for future reuse for backend) and realized that I should be able to restore the program state even after relaunch. Yes, using Room sounds like overhead, but this is the only way. You may also use shared prefs or datastore, but if you want to implement highly structured forms you should save the data in the highly structured way.

huangapple
  • 本文由 发表于 2023年3月31日 23:54:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/75900500.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定