22、DataBinding与表单验证:输入校验规则、错误信息显示、实时验证、提交逻辑
表单验证这件事,说实话,是每个Android开发者都绕不开的坎。我早期做项目时,习惯在Activity里写一堆if-else,每次提交前挨个检查输入框。代码又臭又长,维护起来简直想哭。后来用了DataBinding,配合ObservableField和自定义验证逻辑,整个体验完全不一样了。
这一章,我们就来聊聊怎么用DataBinding优雅地搞定表单验证。包括校验规则怎么写、错误信息怎么展示、如何做到实时验证,以及最终的提交逻辑怎么组织。
核心思路:把验证逻辑从Activity中剥离出来,放到ViewModel或BindingAdapter中。UI只负责展示,数据驱动验证状态。
22.1 基础校验规则定义
先说说校验规则。我个人习惯把规则定义成一个数据类,每个字段对应一个验证结果。这样结构清晰,也方便扩展。
data class FieldValidation(
val errorMessage: String = "",
val isValid: Boolean = true
)
data class LoginFormState(
val username: FieldValidation = FieldValidation(),
val password: FieldValidation = FieldValidation(),
val isFormValid: Boolean = false
)
你可能会问:为什么不直接用String表示错误信息?嗯,这里有个坑。如果只用String,空字符串表示无错误,那你就没法区分「还没验证」和「验证通过」这两种状态。用FieldValidation包装一下,语义更明确。
接下来,在ViewModel里定义验证方法:
class LoginViewModel : ViewModel() {
val formState = ObservableField(LoginFormState())
val username = ObservableField("")
val password = ObservableField("")
fun validateUsername(): FieldValidation {
val value = username.get() ?: ""
return when {
value.isBlank() -> FieldValidation("用户名不能为空", false)
value.length < 3 -> FieldValidation("用户名至少3个字符", false)
else -> FieldValidation("", true)
}
}
fun validatePassword(): FieldValidation {
val value = password.get() ?: ""
return when {
value.isBlank() -> FieldValidation("密码不能为空", false)
value.length < 6 -> FieldValidation("密码至少6个字符", false)
else -> FieldValidation("", true)
}
}
}
小技巧:我在项目中遇到过一种情况——多个字段共用同一个验证规则。比如邮箱和手机号都需要「非空+格式校验」。这时候我会抽一个通用的Validator工具类,避免重复代码。
22.2 错误信息显示:BindingAdapter的妙用
错误信息怎么显示到界面上?最直接的方式是用TextView展示。但我不想在XML里写一堆visibility判断。用BindingAdapter就舒服多了。
@BindingAdapter("app:errorText")
fun TextView.setErrorText(validation: FieldValidation?) {
validation?.let {
text = it.errorMessage
visibility = if (it.isValid) View.GONE else View.VISIBLE
}
}
XML里这样用:
<TextView
android:id="@+id/tvUsernameError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/red"
app:errorText="@{viewModel.formState.username}" />
你看,一行绑定就搞定了错误信息的显示和隐藏。不用写if-else,不用操作findViewById。这就是DataBinding的魅力。
注意:BindingAdapter方法名不能随便起。我建议统一用"app:errorText"这样的命名规范,团队成员一看就知道是干什么的。曾经有个同事起名叫"app:showError",结果项目里好几个同名方法,编译报错查了半天……
22.3 实时验证:TextWatcher的替代方案
实时验证,说白了就是用户一边输入一边检查。传统做法是给EditText加TextWatcher,但代码量不小。用DataBinding的话,我们可以借助ObservableField的监听机制。
在ViewModel里,我习惯这样写:
init {
// 监听用户名变化,实时验证
username.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
val currentState = formState.get() ?: return
val usernameValidation = validateUsername()
formState.set(currentState.copy(username = usernameValidation))
}
})
// 密码同理
password.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
val currentState = formState.get() ?: return
val passwordValidation = validatePassword()
formState.set(currentState.copy(password = passwordValidation))
}
})
}
XML里绑定EditText:
<EditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.username}" />
为什么这样就能实时验证?因为@={}是双向绑定,用户输入改变username的值,触发回调,然后更新formState,UI自动刷新错误信息。整个链条是自动的。
核心逻辑流程图:
22.4 提交逻辑:整体校验与防重复提交
提交按钮的逻辑,我一般分三步:
- 整体校验:调用所有字段的验证方法,更新formState
- 检查结果:如果任何一个字段无效,阻止提交
- 防重复提交:提交过程中禁用按钮
代码实现:
fun onSubmit() {
// 1. 整体校验
val usernameValidation = validateUsername()
val passwordValidation = validatePassword()
val currentState = formState.get() ?: return
formState.set(currentState.copy(
username = usernameValidation,
password = passwordValidation,
isFormValid = usernameValidation.isValid && passwordValidation.isValid
))
// 2. 检查结果
if (!formState.get()?.isFormValid!!) {
// 有错误,不提交
return
}
// 3. 防重复提交(用LiveData或状态标记)
_isLoading.value = true
viewModelScope.launch {
try {
// 执行网络请求或数据库操作
delay(2000) // 模拟耗时
} finally {
_isLoading.value = false
}
}
}
XML里绑定提交按钮:
<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="提交"
android:enabled="@{!viewModel.isLoading}"
android:onClick="@{() -> viewModel.onSubmit()}" />
避坑指南:我曾经在项目里直接用android:enabled="@{viewModel.formState.isFormValid}"来控制按钮可用状态。结果发现一个问题——用户还没输入任何内容时,按钮就是禁用的,体验很差。后来我改成:初始状态按钮可用,点击提交时才做校验。这样用户可以先填,填完再点提交,更符合直觉。
22.5 完整示例:登录表单
把上面所有知识点串起来,一个完整的登录表单验证就出来了。我直接贴核心代码:
// LoginViewModel.kt
class LoginViewModel : ViewModel() {
val username = ObservableField("")
val password = ObservableField("")
val formState = ObservableField(LoginFormState())
val isLoading = ObservableBoolean(false)
init {
username.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
validateField("username")
}
})
password.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
validateField("password")
}
})
}
private fun validateField(field: String) {
val currentState = formState.get() ?: return
val newState = when (field) {
"username" -> currentState.copy(username = validateUsername())
"password" -> currentState.copy(password = validatePassword())
else -> currentState
}
formState.set(newState)
}
private fun validateUsername(): FieldValidation {
val value = username.get() ?: ""
return when {
value.isBlank() -> FieldValidation("用户名不能为空", false)
value.length < 3 -> FieldValidation("用户名至少3个字符", false)
else -> FieldValidation("", true)
}
}
private fun validatePassword(): FieldValidation {
val value = password.get() ?: ""
return when {
value.isBlank() -> FieldValidation("密码不能为空", false)
value.length < 6 -> FieldValidation("密码至少6个字符", false)
else -> FieldValidation("", true)
}
}
fun onSubmit() {
// 整体校验
val usernameValidation = validateUsername()
val passwordValidation = validatePassword()
val currentState = formState.get() ?: return
formState.set(currentState.copy(
username = usernameValidation,
password = passwordValidation
))
if (!usernameValidation.isValid || !passwordValidation.isValid) {
return
}
isLoading.set(true)
viewModelScope.launch {
try {
// 模拟登录请求
delay(1500)
} finally {
isLoading.set(false)
}
}
}
}
<!-- activity_login.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="viewModel" type="com.example.LoginViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="用户名"
android:text="@={viewModel.username}" />
<TextView
android:id="@+id/tvUsernameError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/red"
android:textSize="12sp"
app:errorText="@{viewModel.formState.username}" />
<EditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码"
android:inputType="textPassword"
android:text="@={viewModel.password}" />
<TextView
android:id="@+id/tvPasswordError"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/red"
android:textSize="12sp"
app:errorText="@{viewModel.formState.password}" />
<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登录"
android:enabled="@{!viewModel.isLoading}"
android:onClick="@{() -> viewModel.onSubmit()}" />
</LinearLayout>
</layout>
总结一下:DataBinding做表单验证的核心就是「数据驱动」。你把验证逻辑放在ViewModel里,UI只负责展示和触发。错误信息用BindingAdapter自动显示隐藏,实时验证靠ObservableField的回调,提交逻辑做好整体校验和防重复。这套模式我用了好几年,在多个项目里验证过,稳定且易维护。
嗯,这一章的内容就到这里。表单验证看起来简单,但细节不少。尤其是实时验证和提交逻辑的配合,稍不注意就会出bug。希望你能在实际项目中多练练,找到最适合自己团队的写法。
公众号:蓝海资料掘金营,微信deep3321