学习Jetpack Compose中的高级表单操作

1,005 阅读7分钟

Jetpack Compose中的高级表单操作

在开发一个应用程序时,编写不重复的代码总是一个好主意。这一原则被称为DRY。它与代码解耦携手并进。

这两者确保代码易于扩展,因为应用程序的一个部分的变化不会影响其他不相关的区域。一个修改是在各个组件/模块中统一进行的。

在这篇文章中,我们将看一下在Jetpack compose中处理文本字段的DRY方法。

在文章的最后,你应该。

  • 知道如何在Jetpack compose中处理文本字段。这涉及到状态管理和验证。
  • 知道如何解耦文本字段的操作。
  • 知道如何抽象出各种form 活动。

前提条件

为了舒适地跟进本教程,你需要。

  • Android studio 4.2(Arctic Fox)及以上版本。
  • Kotlin语言的知识。
  • 拥有Jetpack compose的知识。
  • 运行应用程序的物理设备或仿真器。

第1步:设置

为了开始,打开Android Studio,使用 "空的编排活动 "模板创建一个新项目。给它起一个你想要的名字。你可以根据自己的喜好去定制主题。

第2步:表单操作

在开发移动应用程序时,我们很可能会遇到Forms 或某种形式的输入来收集用户的数据。有了Jetpack compose,我们有了可由Material组成的TextField

去吧,把它添加到你的屏幕上。

@Composable
fun Screen(){
    Column {
        TextField(value = "", onValueChange = {})
    }
}

一旦你运行你的应用程序,你会注意到在输入框中输入时没有变化。这是因为你没有在onValueChange 回调中更新该字段的状态。

你可以在官方文档中阅读更多关于状态管理的内容。所以,继续下去,更新Form 组合。

@Composable
fun Screen(){
    var name by remember { mutableStateOf("") }
    Column {
        TextField(value = name, onValueChange = { value -> name = value })
    }
}

正如你所看到的,你正在通过定义一个State<String> ,然后在值发生变化时更新它来管理字段的状态。让我们继续添加另一个字段来获取电子邮件地址和按钮来提交我们的详细信息。

@Composable
fun Screen(){
    var name by remember { mutableStateOf("") }
    var email by remember { mutableStateOf("") }

    Column {
        TextField(
            value = name,
            modifier = Modifier.padding(10.dp),
            onValueChange = { value -> name = value }
        )
        TextField(
            value = email,
            modifier = Modifier.padding(10.dp),
            onValueChange = { value -> email = value },
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
        )
        Button(onClick = { toast(message = "Form: name is $name and email is $email")}) {
            Text("Submit")
        }
    }
}

// the toast extension function
fun Context.toast(message: String){
    Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

在提交后运行验证是很常见的,以确保数据的有效性。对于我们的情况,我们可以运行一个验证,以检查name 字段是否为空,以及email 字段是否符合所需的重码。

我们还需要显示必要的错误,以通知用户这些问题。让我们继续修改Form 的可组合性,如下所示。

@Composable
fun Screen(){
    var name by remember { mutableStateOf("") }
    var nameHasError by remember { mutableStateOf(false) }
    var nameLabel by remember { mutableStateOf("Enter your name") }

    var email by remember { mutableStateOf("") }
    var emailHasError by remember { mutableStateOf(false) }
    var emailLabel by remember { mutableStateOf("Enter your email address") }

    Column {
        TextField(
            value = name,
            isError = nameHasError,
            label = { Text(text = nameLabel) },
            modifier = Modifier.padding(10.dp),
            onValueChange = { value -> name = value }
        )
        TextField(
            value = email,
            isError = emailHasError,
            label = { Text(text = emailLabel) },
            modifier = Modifier.padding(10.dp),
            onValueChange = { value -> email = value },
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
        )
        Button(onClick = {}) {
            Text("Submit")
        }
    }
}

在按钮的onClick lambda中,添加以下内容。

when {
    name.isEmpty() -> {
        nameHasError = true
        nameLabel = "Name cannot be empty"
    }
    !Patterns.EMAIL_ADDRESS.matcher(email).matches() -> {
        emailHasError = true
        emailLabel = "Invalid email address"
    }
    else -> toast(message = "All fields are valid!")
}

这就是我们的字段的一个简单验证过程。然而,我们只为两个输入字段编写了大量的代码。想象一下,如果我们有十个字段,每个字段都有不同的验证过程。如果我们在不同的屏幕上有表单,情况会更糟,这在大多数应用程序中是很常见的。

一个变通的办法是创建一个可以处理表单验证的表单组合。我们可以为Form composable创建一个状态,处理我们传递给它的所有字段。

让我们继续下去,实现它。

第3步:验证器

我们可以从验证器开始。它们在工作方式上都是相似的。它们根据传递的值是否符合特定的标准来返回布尔值。继续创建一个名为Validator 的密封接口。我们所有的验证器都将是这种类型的。

private const val EMAIL_MESSAGE = "invalid email address"
private const val REQUIRED_MESSAGE = "this field is required"
private const val REGEX_MESSAGE = "value does not match the regex"

sealed interface Validator
open class Email(var message: String = EMAIL_MESSAGE): Validator
open class Required(var message: String = REQUIRED_MESSAGE): Validator
open class Regex(var message: String, var regex: String = REGEX_MESSAGE): Validator

每个验证器都将收到一个可选的消息。如果我们愿意的话,这将允许我们传入自定义消息。否则,我们将显示默认的消息。

我们有一个regex 验证器,它接收我们将用来与表单字段的值进行比较的regex。

第4步:为字段创建状态

这一步将创建一个内部状态,它将验证各个输入字段。它还将负责在用户输入时更新该字段。

我们将添加更多的功能来帮助我们管理字段,比如清除文本字段,从字段中获取值,以及显示/隐藏错误。

class Field(val name: String, val label: String = "", val validators: List<Validator>){
    var text: String by mutableStateOf("")
    var lbl: String by mutableStateOf(label)
    var hasError: Boolean by mutableStateOf(false)

    fun clear(){ text = "" }

    private fun showError(error: String){
        hasError = true
        lbl = error
    }

    private fun hideError(){
        lbl = label
        hasError = false
    }

    @Composable
    fun Content(){
        TextField(
            value = text,
            isError = hasError,
            label = { Text(text = lbl) },
            modifier = Modifier.padding(10.dp),
            onValueChange = { value ->
                hideError()
                text = value
            }
        )
    }

    fun validate(): Boolean {
        return validators.map {
            when (it){
                is Email -> true
                is Required -> true
                is Regex -> true
            }
        }.all { it }
    }
}

我们已经创建了一个接收三个参数的类。name 将用于以后将字段与值联系起来。label 将用于设置字段的标签和错误信息。validators 将列出为该字段指定的所有验证器。

可组合的函数Content 将在Form 。函数validate 将返回一个布尔值来表示该字段的值是否有效。它将循环浏览验证器,根据验证器检查值是否正确,并返回真或假。

一旦用户开始编辑表单字段,我们就会清除错误,所以字段会回到无效的状态。为验证器添加以下实现。

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 Regex -> {
                if (!it.regex.toRegex().containsMatchIn(text)){
                    showError(it.message)
                    return@map false
                }
                true
            }
        }
    }.all { it }
}

该函数应该从验证过程中返回匹配的布尔值。

第5步:为表单创建状态

这个表单状态将绘制我们的表单字段,并验证所有传递到它的字段。继续创建一个名为FormState 的类。

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 getData(): Map<String, String> = fields.map { it.name to it.text }.toMap()
}

如果任何字段的验证没有通过,这里的validate 函数也会返回一个布尔值。我们在这一点上中断循环,以节省资源。getData 函数将表单字段的名称映射到相应的值上,以便于接收端的管理。

第6步:把它放在一起

创建一个Form 组合,接收两个参数。

  • 状态:这将是我们上面刚创建的FormState 类的一个实例。
  • 字段:这将是一个表单字段的列表,包括它们各自的验证器。
@Composable
fun Form(state: FormState, fields: List<Field>){
    state.fields = fields

    Column {
        fields.forEach {
            it.Content()
        }
    }
}

我们使用setter方法设置状态的字段。然后我们通过调用字段的Content 组合函数在一列中显示表单字段。这样,我们的表单就完成了😁。要在你的应用程序中使用它,请按以下方式修改你的用户界面。

@Composable
fun Screen(){
    val state by remember { mutableStateOf(FormState()) }

    Column {
        Form(
            state = state,
            fields = listOf(
                Field(name = "username", validators = listOf(Required())),
                Field(name = "email", validators = listOf(Required(), Email()))
            )
        )
        Button(onClick = { if (state.validate()) toast("Our form works!") }) {
            Text("Submit")
        }
    }
}

一旦你运行你的应用程序,一切都应该按预期工作。要获得数据,调用state.getData 方法,你将收到一个表单的地图。

总结

正如你所看到的,单独处理表单会导致大量的重复性代码。但是,通过这种方法,你把表单功能封装到不同的类和可组合物中,使你的工作更容易,代码更简洁。

然而,这种方法也有一些缺点。例如,它没有照顾到字段的自定义排列。你被限制在一列,而你可能希望一些字段在一行。它也不允许修改字段的属性。