2. 支付输入状态模型设计:卡号、有效期、CVV、持卡人姓名状态类型

支付表单的每个输入字段并非简单的字符串——它们各自承载着不同的显示、校验与交互语义。将每个字段的“当前状态”建模为独立且可预测的状态机,是 State Hoisting 落地的第一步。本章定义四个字段的状态类型、数据模型及转换规则。

2.1 状态类型设计原则

  1. 不可变:每个状态对象都是不可变数据类,UI 通过副本更新状态。
  2. 穷举性:所有可能的 UI 表现(空、输入中、校验中、有效、无效、聚焦)都对应明确状态值。
  3. 正交性:每个字段的状态类型独立,字段之间不共享状态枚举。
  4. 可组合:状态携带元数据,例如无效原因、掩码后的显示文本。
讲师提示:四个字段的校验规则差异很大,但状态骨架一致。推荐使用泛型 FieldState<T> 统一建模,再针对每个字段补充专用子类。

2.2 字段状态枚举与语义

状态 语义 适用字段
Idle 字段为空,未获得焦点,无校验请求 全部
Focused 用户正在输入,暂不显示校验错误 全部
Validating 正在进行异步校验(如 Luhn 检查、过期日期验证) 卡号、有效期
Valid 校验通过,字段内容正确 全部
Invalid 校验失败,携带错误消息 全部
Formatted 自动格式化的显示值(如卡号分组显示) 卡号、有效期(MM/YY)

2.3 数据模型定义(Kotlin 风格)

// 泛型状态基类
sealed class FieldState<out T> {
    data class Idle(val raw: String = "") : FieldState<Nothing>()
    data class Focused(val raw: String) : FieldState<Nothing>()
    data class Validating(val raw: String) : FieldState<Nothing>()
    data class Valid<T>(val value: T) : FieldState<T>()
    data class Invalid(val raw: String, val error: String) : FieldState<Nothing>()
    data class Formatted(val raw: String, val display: String) : FieldState<Nothing>()
}

// 具体字段类型
typealias CardNumberState = FieldState<String>           // Valid 时携带已校验的卡号
typealias ExpiryState = FieldState<Pair<Int, Int>>        // (月, 年)
typealias CvvState = FieldState<String>
typealias CardHolderState = FieldState<String>
注意:不要在同一字段状态中混合“原始值”与“校验结果”。例如 Valid 只携带经过校验并转换后的原始值,UI 需从 Formatted 或 Focused 获取显示文本。职责分离可避免状态膨胀。

2.4 状态模型结构图

支付输入状态模型结构 卡号状态 Luhn 校验 分组格式化 有效期状态 季度/过期待定 MM/YY 格式化 CVV 状态 3/4 位数字校验 仅数字过滤 持卡人姓名状态 字母+空格校验 大小写自动转换 公共状态枚举 Idle、Focused、Validating、Valid、Invalid、Formatted 每个字段独立持有这些状态实例,但泛型 sealed class 共享相同骨架 状态转换说明 “失焦后自动触发校验” → Idle/Focused → Validating → Valid / Invalid “输入变化时保持 Focused,清空时回到 Idle”

2.5 常见错误与调试

  • 状态未映射聚焦状态:只定义 Valid/Invalid,导致 UI 无法区分“正在输入”与“已完成输入”。表现为输入框永远显示错误红线。
  • 原始值与校验值混用:在 Valid 状态下依然存储原始字符串,格式化显示时重新处理,破坏不可变性。应使用 Formatted 状态携带显示字符串。
  • 状态复制导致 CPU 抖动:每次输入变更都创建全新的 State 对象,对 Compose 来说是正常的;但若在 derivedStateOf 或 Snapshot 中频繁触发校验,会造成帧率下降。可在 Validating 状态下加入防抖(debounce)。
调试建议:在 ViewModel 或 Composable 中打印状态变化日志,例如 Log.d("PaymentState", "card: $cardState"),配合 Compose 的 Live Edit 观察状态机是否按预期转换。特别注意 Idle → Focused → Validating → Valid/Invalid 路径是否完整。

2.6 性能与稳定性优化

  • 使用 @Stable 注解:为状态数据类标记 @Stable,帮助 Compose 跳过不必要的重组。
  • 字段级状态提升:每个字段持有自己的 MutableState<FieldState>,而不是将整个表单合并为一个 State 对象。减少无关字段的联动重组。
  • 校验逻辑延迟执行:非聚焦且非空时才触发后端 Luhn 校验;聚焦状态仅做前端格式过滤,避免网络请求打断输入。
// 优化示例:卡号状态副本仅在进入 Validating 时启动校验协程
fun CvvState.validateAsync() {
    if (this !is FieldState.Focused) return
    // 启动协程:模拟 200ms 后返回 Valid 或 Invalid
}