当前位置: 首页 > news >正文

【Android之路】 Kotlin 的 data class、enum class、sealed interface

这篇文章分两部分:

  1. 语言层:什么是 data classenum classsealed interface,各自解决什么问题;
  2. 架构层:在 Android(以 Compose 为例)如何用状态驱动 UI,把 UI 和业务逻辑分层组合起来(含完整示例代码)。

一、data class:为“承载数据”的类而生

适用场景:对象主要用来“存数据”,几乎没复杂行为。
好处:自动生成 equals/hashCode/toString/copy/componentN,写得少,判等、打印、复制都方便。

data class User(val id: String,val name: String,val age: Int
)val u1 = User("1", "Tom", 18)
println(u1)                 // User(id=1, name=Tom, age=18)
val u2 = u1.copy(age = 20)  // 局部复制
println(u1 == u2)           // false(值比较)

常见误区

  • data class 里塞大量可变状态与业务逻辑,后期难测难维护。建议:保持“哑数据”定位,让行为放到其他层(例如 use case / reducer)。

二、enum class:一组有限常量的类型安全表达

适用场景:一组固定离散的取值,例如运算符、方向、主题模式。
好处:与 when 搭配,分支穷举、编译期检查更安全。

enum class Op { Add, Sub, Mul, Div }fun apply(a: Int, b: Int, op: Op): Int = when (op) {Op.Add -> a + bOp.Sub -> a - bOp.Mul -> a * bOp.Div -> a / b
}

常见误区

  • 用字符串/整型常量代替枚举,丢失类型约束,when 分支漏写编译也不会提醒。

三、sealed interface(或 sealed class):受限层级的“代数数据类型”

适用场景:你要表达“一类事物的若干封闭变体”,常用于事件流、UI 状态、网络结果(Success/Error/Loading)。
好处:所有实现类都在同一编译单元内已知,when 能做穷举校验

sealed interface CalcEvent {data class Digit(val ch: Char) : CalcEventdata object Dot : CalcEventdata class Operator(val op: Op) : CalcEventdata object Equals : CalcEventdata object AllClear : CalcEventdata object ToggleSign : CalcEventdata object Percent : CalcEvent
}

sealed 与 enum 的取舍

  • 当“变体”只需名称(无额外字段)→ enum
  • 当“变体”需要携带不同数据(比如 Digit 要带 ch)→ sealed 更合适。

四、把语言特性带进 Android 架构:UI = f(State)

1) 思想要点

  • UI 是状态的函数:UI 不直接改数据,只显示 State
  • 单向数据流:UI 发送 Event → 业务层(Reducer/UseCase)计算新 State → UI 订阅并重组;
  • ViewModel 托管状态:抗配置变更,便于测试。

2) 我们做一个迷你“计算器”来演示

功能:数字、点、小数拼接、四则运算、等号、AC、±、%(核心);
分层:纯业务(Reducer) + ViewModel + Compose UI


五、领域模型(data/enum/sealed 的组合拳)

// 运算符(有限集合)——用 enum
enum class Op { Add, Sub, Mul, Div }// 页面业务状态(承载数据)——用 data class
data class CalcState(val display: String = "0",val leftOperand: String? = null,val op: Op? = null,val inTypingRight: Boolean = false,val hasResult: Boolean = false,val error: Boolean = false
)// 事件流(变体携带不同数据)——用 sealed interface
sealed interface CalcEvent {data class Digit(val ch: Char) : CalcEventdata object Dot : CalcEventdata class Operator(val op: Op) : CalcEventdata object Equals : CalcEventdata object AllClear : CalcEventdata object ToggleSign : CalcEventdata object Percent : CalcEvent
}

六、业务核心(Reducer:Event -> State),纯 Kotlin、可单测

只展示骨架,细节同理扩展;它不依赖 Android/Compose,测试更轻松。

fun reduce(state: CalcState, event: CalcEvent): CalcState = when (event) {is CalcEvent.Digit    -> onDigit(state, event.ch)CalcEvent.Dot         -> onDot(state)is CalcEvent.Operator -> onOperator(state, event.op)CalcEvent.Equals      -> onEquals(state)CalcEvent.AllClear    -> CalcState()CalcEvent.ToggleSign  -> onToggleSign(state)CalcEvent.Percent     -> onPercent(state)
}private fun onDigit(state: CalcState, ch: Char): CalcState { /* 处理前导0、结果后重启输入…… */ return state }
private fun onDot(state: CalcState): CalcState { /* 保证只出现一个 '.' */ return state }
private fun onOperator(state: CalcState, op: Op): CalcState { /* 固化左操作数/替换运算符/连算 */ return state }
private fun onEquals(state: CalcState): CalcState { /* 左右齐备→计算→结果或错误 */ return state }
private fun onToggleSign(state: CalcState): CalcState { /* 切换 +/-,统一 -0→0 */ return state }
private fun onPercent(state: CalcState): CalcState { /* 当前显示 ÷ 100 */ return state }

关键点:所有规则都在 Reducer,UI 不参与计算、只发事件。


七、ViewModel:Android 世界与纯业务的“桥”

class CalculatorViewModel : ViewModel() {var state by mutableStateOf(CalcState()) // Compose 可观察private setfun onEvent(event: CalcEvent) {state = reduce(state, event)}
}
  • 为什么放 VM? 旋转/后台回来不丢状态;UI 订阅 state 就能自动刷新。
  • 你也可以用 StateFlow<CalcState>,更贴近 MVI。

八、UI(Jetpack Compose):数据驱动、无业务

1) UI 模型(非业务)

把键盘当成数据来渲染,data class 承载 UI 信息:

enum class KeyKind { Digit, Operator, Action, Equals }data class KeySpec(val label: String,val kind: KeyKind,val op: Op? = null,val span: Int = 1        // “0” 占两格
)

2) 键盘蓝图(数据驱动布局)

val KEYS: List<List<KeySpec>> = listOf(listOf(KeySpec("AC", KeyKind.Action), KeySpec("+/-", KeyKind.Action), KeySpec("%", KeyKind.Action), KeySpec("÷", KeyKind.Operator, Op.Div)),listOf(KeySpec("7", KeyKind.Digit), KeySpec("8", KeyKind.Digit), KeySpec("9", KeyKind.Digit), KeySpec("×", KeyKind.Operator, Op.Mul)),listOf(KeySpec("4", KeyKind.Digit), KeySpec("5", KeyKind.Digit), KeySpec("6", KeyKind.Digit), KeySpec("−", KeyKind.Operator, Op.Sub)),listOf(KeySpec("1", KeyKind.Digit), KeySpec("2", KeyKind.Digit), KeySpec("3", KeyKind.Digit), KeySpec("+", KeyKind.Operator, Op.Add)),listOf(KeySpec("0", KeyKind.Digit, span = 2), KeySpec(".", KeyKind.Action), KeySpec("=", KeyKind.Equals))
)

3) UI 组件(无业务,只显示+上报)

@Composable
fun CalculatorScreen(vm: CalculatorViewModel) {Column(Modifier.padding(12.dp)) {DisplayArea(vm.state.display)Keypad(KEYS) { spec ->keySpecToEvent(spec)?.let(vm::onEvent)}}
}@Composable fun DisplayArea(text: String) = Text(text, fontSize = 40.sp, textAlign = TextAlign.End, modifier = Modifier.fillMaxWidth())@Composable fun Keypad(keys: List<List<KeySpec>>, onKey: (KeySpec) -> Unit) {Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {keys.forEach { row -> KeyRow(row, onKey) }}
}
@Composable fun KeyRow(row: List<KeySpec>, onKey: (KeySpec) -> Unit) {Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {row.forEach { spec -> KeyButton(spec, onKey, Modifier.weight(spec.span.toFloat())) }}
}
@Composable fun KeyButton(spec: KeySpec, onClick: (KeySpec) -> Unit, modifier: Modifier = Modifier) {Button(onClick = { onClick(spec) }, modifier = modifier.padding(4.dp)) {Text(spec.label, fontSize = 22.sp)}
}

4) UI→事件的映射(把 KeySpec 转成业务事件)

fun keySpecToEvent(spec: KeySpec): CalcEvent? = when (spec.kind) {KeyKind.Digit   -> spec.label.singleOrNull()?.takeIf { it.isDigit() }?.let { CalcEvent.Digit(it) }KeyKind.Action  -> when (spec.label) { "AC" -> CalcEvent.AllClear; "+/-" -> CalcEvent.ToggleSign; "%" -> CalcEvent.Percent; "." -> CalcEvent.Dot; else -> null }KeyKind.Operator-> spec.op?.let { CalcEvent.Operator(it) }KeyKind.Equals  -> CalcEvent.Equals
}

注意:UI 只做映射和上报,不做任何数值计算。


九、组合到 Activity(不依赖 compose 的 viewModel() 扩展也能用)

class MainActivity : ComponentActivity() {private val vm: CalculatorViewModel by viewModels() // Activity 侧获取 VMoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {MyTheme {CalculatorScreen(vm) // 显式传入,简单稳妥}}}
}

十、为什么这种分层更“耐用”?

  • 可测试:Reducer 是纯函数,直接喂事件断言新状态即可;UI 测不了业务。
  • 解耦:UI 改外观不动业务;业务规则调整不改 UI。
  • 稳健:状态集中在 VM,配置变更不丢;事件是封闭集合(sealed),规则覆盖清晰。
  • 可扩展:想加“历史记录/科学计算/横屏双列”,改 UI 蓝图或扩展事件与状态即可。

十一、单元测试示例(Reducer)

class CalcReducerTest {@Test fun `12 plus 3 equals 15`() {var s = CalcState()s = reduce(s, CalcEvent.Digit('1'))s = reduce(s, CalcEvent.Digit('2'))s = reduce(s, CalcEvent.Operator(Op.Add))s = reduce(s, CalcEvent.Digit('3'))s = reduce(s, CalcEvent.Equals)assertEquals("15", s.display)}
}

十二、常见坑位与对策

  1. 把 UI 和业务搅在一起 → 无法单测。

    • 对策:业务写进 Reducer/UseCase,UI 只发事件、订阅状态。
  2. 浮点误差0.1 + 0.2 != 0.3

    • 对策:内部用 BigDecimal,显示前统一 format
  3. 重复小数点/连续运算符 → “1…2”、“1++2”。

    • 对策:在 onDot/onOperator 分支做约束,必要时“替换运算符”。
  4. 等号后输入 → 结果被拼接。

    • 对策:hasResult 标记,等号后第一位数字重启输入。
  5. 除 0 → 崩溃或 NaN。

    • 对策:safeCompute 捕获并进入错误态,数字键恢复。

结语

  • data classenum classsealed interface 是 Kotlin 建模的“三剑客”。
  • 在 Android(Compose)里,结合 State → UIEvent → Reducer → State 的单向数据流,用它们可以写出清晰、可测、可维护的 App。
  • 这套模式不仅适用于计算器,也适用于表单、列表筛选、复杂对话框、甚至多步骤流程。
http://www.dtcms.com/a/423866.html

相关文章:

  • 公司网站注册要多少钱网页设计作业 介绍家乡
  • [特殊字符]函数指针:C语言的动态灵魂,嵌入式的超能力(202589)
  • 海口网站建设高端asp.net做电商网站
  • C++ 面向对象进阶:继承深化与多态详解
  • 达建网站长沙网站快速排名优化
  • 网站浏览器兼容性问题吗产品介绍网站源码
  • 20.Nginx 服务器
  • CTFshow萌新杂项详细解题攻略及学习笔记
  • jsp网站服务器如何做防护飘云网络科技有限公司
  • Effective Python 第34条: 避免使用 `send()` 给生成器注入数据
  • wordpress站内301上海对外经贸大学
  • 当AI助手“记忆混乱”:理解与应对Roo Code的上下文污染问题
  • Docker 网络详解:(二)虚拟网络环境搭建与测试
  • 【Docker】在项目中如何实现Dockerfile 文件编写
  • 专门做任务的网站吗wordpress数据库文件
  • AMD KFD的BO设计分析系列5-3:VM-amdgpu_bo_va_mapping
  • FilterSolutions2019使用指南
  • 方寸控股解读:《工业园区高质量发展指引》下的园区升级路径
  • 学习总结——接口测试基础
  • 好的案例展示网站在线设计平台招募设计师
  • 阳泉网站建设哪家便宜上海哪家公司提供专业的网站建设
  • TCP的理解
  • 鸿蒙应用主题模式切换实现详解
  • Matplotlib `imsave()` 函数详解
  • NFC技术如何破解电子制造领域的效率瓶颈与追溯难题
  • sk06.【scikit-learn基础】--『监督学习』之决策树
  • 银川怎么做网站wordpress炫酷站
  • 网站说明页命名大连响应式网站建设
  • 程序综合实践第二次递归与dfs
  • 半双工 vs 全双工:对讲机与电话的根本区别