4. 输入验证框架:Validator接口设计、正则表达式校验、自定义校验规则、错误提示显示
4.1 验证框架的核心原理与架构
输入验证是安全输入组件的第一道防线。统一的验证框架将校验逻辑从UI代码中抽离,形成可复用、可测试、可扩展的校验层。Material 3设计规范强调错误状态应即时、明确且具有可操作性,因此验证框架不仅要判断“对与错”,还要承载错误类型、错误位置和修复建议等语义信息。
验证框架的核心职责可归纳为四个层级:
| 层级 | 职责 | 对应模块 |
|---|---|---|
| 接口层 | 定义验证契约,统一入参/出参 | Validator<T> 接口 |
| 规则层 | 实现具体校验逻辑 | RegexValidator / CustomValidator |
| 聚合层 | 组合多规则,收集错误 | CompositeValidator / ErrorCollector |
| 展示层 | 将错误映射为Material 3提示 | ValidationErrorHandler |
验证框架的设计原则:单一职责(每个Validator只做一种校验)、开闭原则(新增规则不修改现有代码)、依赖倒置(高层模块依赖接口而非具体实现)。
在Material 3 Compose中,
TextField的isError与supportingText属性天然支持验证框架的展示层对接,无需额外封装。下图展示了验证框架的完整数据流与各组件协作关系:
4.2 Validator接口设计
Validator是验证框架的顶层抽象,基于Kotlin的泛型与函数式特性,定义最简契约:
interface Validator<in T> {
/**
* 执行验证,返回验证结果
* @param input 待验证的输入值
* @return ValidationResult 包含是否通过及错误信息列表
*/
fun validate(input: T): ValidationResult
}
data class ValidationResult(
val isValid: Boolean,
val errors: List<ValidationError> = emptyList()
)
data class ValidationError(
val code: String, // 错误码,如 "EMAIL_INVALID"
val message: String, // 用户可读的错误描述
val field: String? = null // 关联字段名,用于多字段表单
)
接口使用
in关键字实现逆变,使Validator支持父类型输入,提升复用性。例如Validator<CharSequence>可同时验证String与Editable。4.3 正则表达式校验器实现
正则校验器是最常用的内置实现,通过Pattern预编译提高性能,支持常见的邮箱、手机号、密码强度等场景:
class RegexValidator(
private val pattern: String,
private val errorCode: String,
private val errorMessage: String,
private val flags: Int = 0
) : Validator<CharSequence> {
private val compiledPattern: Pattern by lazy {
Pattern.compile(pattern, flags)
}
override fun validate(input: CharSequence): ValidationResult {
val matcher = compiledPattern.matcher(input)
return if (matcher.matches()) {
ValidationResult(isValid = true)
} else {
ValidationResult(
isValid = false,
errors = listOf(ValidationError(errorCode, errorMessage))
)
}
}
}
// 预置常用规则
object CommonPatterns {
const val EMAIL = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"
const val PHONE_CN = "^1[3-9]\\d{9}$"
const val PASSWORD_WEAK = "^.{6,}$"
const val PASSWORD_STRONG = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@\$!%*?&])[A-Za-z\\d@\$!%*?&]{8,}$"
}
正则表达式中的反斜杠在Kotlin字符串中需要双重转义(
\\d)。建议将常用模式定义为常量,并在单元测试中验证其正确性。4.3.1 API调用示例
val emailValidator = RegexValidator(
pattern = CommonPatterns.EMAIL,
errorCode = "EMAIL_INVALID",
errorMessage = "请输入有效的邮箱地址"
)
val result = emailValidator.validate("test@example.com")
if (!result.isValid) {
result.errors.forEach { error ->
Log.e("Validation", "[${error.code}] ${error.message}")
}
}
4.4 自定义校验规则
当内置RegexValidator无法满足业务语义时,通过实现Validator接口创建自定义校验器。以下示例校验用户名是否包含敏感词:
class SensitiveWordValidator(
private val blacklist: Set<String>,
private val errorCode: String = "SENSITIVE_WORD",
private val errorMessage: String = "输入内容包含敏感词汇"
) : Validator<CharSequence> {
override fun validate(input: CharSequence): ValidationResult {
val matched = blacklist.firstOrNull { word ->
input.contains(word, ignoreCase = true)
}
return if (matched == null) {
ValidationResult(isValid = true)
} else {
ValidationResult(
isValid = false,
errors = listOf(ValidationError(
code = errorCode,
message = "$errorMessage: $matched"
))
)
}
}
}
// 使用示例
val wordFilter = SensitiveWordValidator(
blacklist = setOf("admin", "root", "test")
)
val checkResult = wordFilter.validate("user_admin_001")
// checkResult.isValid == false
自定义校验器中不要修改输入对象(不可变设计),避免副作用。若需要转换输入,应在验证前预处理。
4.5 错误提示显示机制
Material 3的错误提示遵循“即时、具体、可操作”三原则。验证框架通过ErrorHandler将ValidationError映射为UI状态:
class ValidationErrorHandler {
fun toTextFieldState(errors: List<ValidationError>): TextFieldErrorState {
return when {
errors.isEmpty() -> TextFieldErrorState.None
else -> TextFieldErrorState.Error(
message = errors.first().message,
errorCode = errors.first().code
)
}
}
}
sealed class TextFieldErrorState {
object None : TextFieldErrorState()
data class Error(
val message: String,
val errorCode: String
) : TextFieldErrorState()
}
// Compose 集成
@Composable
fun ValidatedTextField(
value: String,
onValueChange: (String) -> Unit,
validator: Validator<CharSequence>,
label: String
) {
var errorState by remember { mutableStateOf<TextFieldErrorState>(TextFieldErrorState.None) }
TextField(
value = value,
onValueChange = { newValue ->
onValueChange(newValue)
// 实时校验(可配置防抖)
val result = validator.validate(newValue)
errorState = ValidationErrorHandler().toTextFieldState(result.errors)
},
isError = errorState is TextFieldErrorState.Error,
supportingText = {
if (errorState is TextFieldErrorState.Error) {
Text(
text = (errorState as TextFieldErrorState.Error).message,
color = MaterialTheme.colorScheme.error
)
}
},
label = { Text(label) }
)
}
4.5.1 多错误聚合展示
当同时启用多个校验规则时,错误收集器会聚合所有失败信息:
class CompositeValidator(
private val validators: List<Validator<CharSequence>>
) : Validator<CharSequence> {
override fun validate(input: CharSequence): ValidationResult {
val allErrors = mutableListOf<ValidationError>()
for (v in validators) {
val result = v.validate(input)
if (!result.isValid) {
allErrors.addAll(result.errors)
}
}
return ValidationResult(
isValid = allErrors.isEmpty(),
errors = allErrors
)
}
}
// 使用示例:密码复合校验
val passwordValidator = CompositeValidator(listOf(
RegexValidator(CommonPatterns.PASSWORD_WEAK, "PWD_TOO_SHORT", "密码至少6位"),
RegexValidator(".*[A-Z].*".toRegex().pattern, "PWD_NO_UPPER", "需包含大写字母"),
RegexValidator(".*\\d.*".toRegex().pattern, "PWD_NO_DIGIT", "需包含数字")
))
4.6 链式调用与DSL配置
通过DSL简化验证规则的配置,提升可读性:
class ValidationRuleSet(private val input: CharSequence) {
private val rules = mutableListOf<Validator<CharSequence>>()
fun regex(pattern: String, code: String, message: String) {
rules.add(RegexValidator(pattern, code, message))
}
fun custom(validator: Validator<CharSequence>) {
rules.add(validator)
}
fun build(): ValidationResult {
return CompositeValidator(rules).validate(input)
}
}
fun validate(input: CharSequence, block: ValidationRuleSet.() -> Unit): ValidationResult {
val set = ValidationRuleSet(input)
set.block()
return set.build()
}
// DSL 调用
val result = validate("MyP@ss1") {
regex(CommonPatterns.PASSWORD_WEAK, "LENGTH", "长度不足6位")
regex(".*[A-Z].*".toRegex().pattern, "UPPER", "需要大写字母")
custom(SensitiveWordValidator(setOf("admin", "root")))
}
4.7 性能优化策略
| 优化点 | 方案 | 收益 |
|---|---|---|
| 正则预编译 | 使用Pattern.compile() + by lazy | 避免每次验证重新编译 |
| 验证时机 | 失焦时验证 或 输入停止后300ms防抖 | 减少高频验证的UI卡顿 |
| 短路策略 | CompositeValidator遇到致命错误立即终止 | 减少无效计算 |
| 结果缓存 | 对不变输入缓存验证结果 | 提升重复验证速度 |
| 异步验证 | 耗时校验(如API查重)使用coroutine | 不阻塞主线程 |
// 防抖验证示例(使用Flow)
fun debouncedValidate(
inputFlow: StateFlow<String>,
validator: Validator<CharSequence>
): Flow<ValidationResult> {
return inputFlow
.debounce(300)
.map { validator.validate(it) }
.distinctUntilChanged()
}
4.8 调试技巧与常见错误
4.8.1 调试技巧
- 日志标记:在Validator中输出
Log.v("Validation", "${this::class.simpleName} → $result"),方便追踪链路。 - 单元测试:为每个Validator编写参数化测试,覆盖正常值、边界值、异常值。
- VisualValidator:开发期在UI上叠加半透明浮层,实时显示当前所有验证状态。
- 断点条件:在
ValidationResult(isValid = false)处设置条件断点,快速定位失败规则。
4.8.2 常见错误清单
| 错误类型 | 现象 | 解决方案 |
|---|---|---|
| 正则转义遗漏 | 运行时抛出PatternSyntaxException | 使用Pattern.quote()包裹字面量部分 |
| 错误提示覆盖 | 多条错误同时闪烁 | 优先展示第一条错误,或聚合为列表 |
| 空指针崩溃 | Validator收到null输入 | 接口入参使用可空类型并做空安全处理 |
| 验证状态残留 | 清除输入后错误提示未消失 | 在onValueChange中检测空值并重置状态 |
| 国际化遗漏 | 错误信息硬编码为中文 | 使用stringResource或资源ID延迟加载 |
// 空安全Validator包装器
class NullSafeValidator<T : Any>(
private val delegate: Validator<T>
) : Validator<T?> {
override fun validate(input: T?): ValidationResult {
return if (input == null) {
ValidationResult(false, listOf(
ValidationError("NULL_INPUT", "输入值不能为空")
))
} else {
delegate.validate(input)
}
}
}
4.9 部署验证与集成检查
将验证框架集成到应用中时,执行以下检查清单:
- 规则覆盖率:所有输入字段均绑定至少一条Validator。
- 错误码唯一性:全局错误码无冲突,便于埋点统计。
- 可测试性:每个Validator可独立注入Mock依赖。
- 性能基线:单次验证耗时 < 2ms(正则类) / < 50ms(网络类)。
- 无障碍支持:错误提示通过
contentDescription传递给屏幕阅读器。
生产环境中建议启用验证结果的上报监控,及时发现高频错误字段,反向优化校验规则或引导文案。
4.10 最佳实践总结
- 将Validator声明为单例或复用实例,避免重复创建。
- 错误码使用
UPPER_SNAKE_CASE格式,与后端错误码对齐。 - 使用
sealed class定义错误状态,确保exhaustive when处理。 - 在ViewModel层调用验证,UI层仅做状态映射,保持职责清晰。
- 单元测试覆盖所有自定义Validator,正则Validator使用
assertDoesNotMatch与assertMatches成对验证。
// ViewModel 层验证示例
class LoginViewModel : ViewModel() {
private val emailValidator = RegexValidator(CommonPatterns.EMAIL, "EMAIL_INVALID", "邮箱格式错误")
fun validateEmail(email: String): ValidationResult {
return emailValidator.validate(email)
}
fun login(email: String, password: String) {
val emailResult = validateEmail(email)
if (!emailResult.isValid) {
_uiState.value = LoginUiState.Error(emailResult.errors.first().message)
return
}
// 继续登录流程...
}
}
验证框架的设计核心在于“分离关注点”:规则定义、规则组合、错误展示、错误处理各自独立,通过Validator接口解耦。这种架构使团队可以并行开发——产品经理调整文案、后端对齐错误码、前端优化UI,互不干扰。