4. 输入验证框架:Validator接口设计、正则表达式校验、自定义校验规则、错误提示显示

4.1 验证框架的核心原理与架构

输入验证是安全输入组件的第一道防线。统一的验证框架将校验逻辑从UI代码中抽离,形成可复用、可测试、可扩展的校验层。Material 3设计规范强调错误状态应即时、明确且具有可操作性,因此验证框架不仅要判断“对与错”,还要承载错误类型、错误位置和修复建议等语义信息。

验证框架的核心职责可归纳为四个层级:

层级职责对应模块
接口层定义验证契约,统一入参/出参Validator<T> 接口
规则层实现具体校验逻辑RegexValidator / CustomValidator
聚合层组合多规则,收集错误CompositeValidator / ErrorCollector
展示层将错误映射为Material 3提示ValidationErrorHandler
验证框架的设计原则:单一职责(每个Validator只做一种校验)、开闭原则(新增规则不修改现有代码)、依赖倒置(高层模块依赖接口而非具体实现)。
在Material 3 Compose中,TextField的isError与supportingText属性天然支持验证框架的展示层对接,无需额外封装。

下图展示了验证框架的完整数据流与各组件协作关系:

输入验证框架流程图 用户输入 Validator接口 正则表达式校验 自定义校验规则 错误收集器 错误提示显示 验证框架数据流

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 部署验证与集成检查

将验证框架集成到应用中时,执行以下检查清单:

  1. 规则覆盖率:所有输入字段均绑定至少一条Validator。
  2. 错误码唯一性:全局错误码无冲突,便于埋点统计。
  3. 可测试性:每个Validator可独立注入Mock依赖。
  4. 性能基线:单次验证耗时 < 2ms(正则类) / < 50ms(网络类)。
  5. 无障碍支持:错误提示通过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,互不干扰。