Java 常用 API 如何建模 Android 订单业务

订单确认页看上去只是把商品名、金额、状态和时间从一个页面带到另一个页面;真正的难点是,这些值会经过 Intent、Bundle、Parcelable,并可能在进程被系统回收后重新创建。若领域状态本身可变、金额用浮点数、枚举没有稳定存储值,页面重建后就很容易得到一个"看着能打开、数据却不可信"的订单。

一句话结论: 把订单建成不可变、可比较且有稳定标识的领域数据;把 Bundle 和 Parcelable 仅视为跨组件传输格式,并在恢复时重新校验和格式化,才能让 Android 的页面跳转与进程重建不破坏业务语义。

先划清版本与事实边界

本文以 Java 8 时间 API 和当前 Android Framework 的公开 Intent、Bundle、Parcel、Parcelable 设计为概念参照。Bundle 的具体存储细节、Binder 限额和系统保存状态时机随 Android 版本及设备实现而不同;它们不是稳定的领域模型契约。标为"源码事实"的内容描述公开 API 的职责,标为"简化伪代码"的内容只展示建模关系,标为"生产建议"的内容需要结合应用架构、数据库和服务端协议落地。

从订单状态开始,而不是从 extra 开始

设想用户在订单详情页点击付款:页面持有一个订单快照,付款结果把状态从待支付推进到已支付,随后详情页把"只够恢复页面"的参数交给物流页。订单状态不是一个随意拼接的字符串,也不是把整个业务对象长期塞在 Bundle 里。
#mermaid-svg-VIQ85RYblXcxg5lp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VIQ85RYblXcxg5lp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VIQ85RYblXcxg5lp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VIQ85RYblXcxg5lp .error-icon{fill:#552222;}#mermaid-svg-VIQ85RYblXcxg5lp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VIQ85RYblXcxg5lp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VIQ85RYblXcxg5lp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VIQ85RYblXcxg5lp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VIQ85RYblXcxg5lp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VIQ85RYblXcxg5lp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VIQ85RYblXcxg5lp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VIQ85RYblXcxg5lp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VIQ85RYblXcxg5lp .marker.cross{stroke:#333333;}#mermaid-svg-VIQ85RYblXcxg5lp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VIQ85RYblXcxg5lp p{margin:0;}#mermaid-svg-VIQ85RYblXcxg5lp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VIQ85RYblXcxg5lp .cluster-label text{fill:#333;}#mermaid-svg-VIQ85RYblXcxg5lp .cluster-label span{color:#333;}#mermaid-svg-VIQ85RYblXcxg5lp .cluster-label span p{background-color:transparent;}#mermaid-svg-VIQ85RYblXcxg5lp .label text,#mermaid-svg-VIQ85RYblXcxg5lp span{fill:#333;color:#333;}#mermaid-svg-VIQ85RYblXcxg5lp .node rect,#mermaid-svg-VIQ85RYblXcxg5lp .node circle,#mermaid-svg-VIQ85RYblXcxg5lp .node ellipse,#mermaid-svg-VIQ85RYblXcxg5lp .node polygon,#mermaid-svg-VIQ85RYblXcxg5lp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VIQ85RYblXcxg5lp .rough-node .label text,#mermaid-svg-VIQ85RYblXcxg5lp .node .label text,#mermaid-svg-VIQ85RYblXcxg5lp .image-shape .label,#mermaid-svg-VIQ85RYblXcxg5lp .icon-shape .label{text-anchor:middle;}#mermaid-svg-VIQ85RYblXcxg5lp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VIQ85RYblXcxg5lp .rough-node .label,#mermaid-svg-VIQ85RYblXcxg5lp .node .label,#mermaid-svg-VIQ85RYblXcxg5lp .image-shape .label,#mermaid-svg-VIQ85RYblXcxg5lp .icon-shape .label{text-align:center;}#mermaid-svg-VIQ85RYblXcxg5lp .node.clickable{cursor:pointer;}#mermaid-svg-VIQ85RYblXcxg5lp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VIQ85RYblXcxg5lp .arrowheadPath{fill:#333333;}#mermaid-svg-VIQ85RYblXcxg5lp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VIQ85RYblXcxg5lp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VIQ85RYblXcxg5lp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VIQ85RYblXcxg5lp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VIQ85RYblXcxg5lp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VIQ85RYblXcxg5lp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VIQ85RYblXcxg5lp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VIQ85RYblXcxg5lp .cluster text{fill:#333;}#mermaid-svg-VIQ85RYblXcxg5lp .cluster span{color:#333;}#mermaid-svg-VIQ85RYblXcxg5lp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VIQ85RYblXcxg5lp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VIQ85RYblXcxg5lp rect.text{fill:none;stroke-width:0;}#mermaid-svg-VIQ85RYblXcxg5lp .icon-shape,#mermaid-svg-VIQ85RYblXcxg5lp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VIQ85RYblXcxg5lp .icon-shape p,#mermaid-svg-VIQ85RYblXcxg5lp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VIQ85RYblXcxg5lp .icon-shape .label rect,#mermaid-svg-VIQ85RYblXcxg5lp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VIQ85RYblXcxg5lp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VIQ85RYblXcxg5lp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VIQ85RYblXcxg5lp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提交订单
支付成功
支付超时
发货
签收
CREATED
PENDING_PAYMENT
PAID
CLOSED
SHIPPED
COMPLETED

这个图表达业务允许的迁移,不代表 UI 可以直接把任意枚举写进状态。比如 CLOSED 不能跳回 PAID,页面按钮应根据当前状态和后端确认结果决定是否发起操作。

String、包装类与空值的边界

String 是不可变对象,适合订单号、货币代码、展示名称和稳定状态码。它的不可变性意味着拼接或替换会产生新对象;不要指望调用方法会原地修改订单字段。Android 的 Intent.putExtra 和 Bundle.putString 传递的是值内容,不能把它们当作共享、可观察的领域对象。

Java 包装类如 Integer、Long、Boolean 让值能表示为对象,并允许空值;但自动装箱并没有消除空指针风险:Long total = null; long cents = total; 会在拆箱时抛出 NullPointerException。业务层应把"未知""未加载""没有资格"建模为明确状态,而不是让每个调用点猜测 null 的含义。进入 Bundle 前也应明确可选字段是否存在,读取时用 containsKey 或给出可靠默认值。

enum 是状态集合,不是数据库协议

enum OrderState 让编译器帮助穷尽分支,并让状态迁移集中可读。不过 name() 是 Java 源码名,重命名常量就会改变持久化文本。因此,落盘、网络和跨版本恢复使用稳定 ID,例如 "pending_payment",再由工厂转换为枚举。未知 ID 不应悄悄映射为某个成功状态,应该产生可见的恢复失败、重新拉取或兼容分支。

时间、金额与资源格式化各做自己的工作

Instant 适合表达一个绝对时刻,例如支付完成时间;LocalDateTime 没有时区,不应独自代表服务端已发生的事件。显示时用用户时区把 Instant 转为 ZonedDateTime,再用 DateTimeFormatter 格式化。格式化器属于展示层,领域模型保存机器可比较的时间值。

金额不要使用 double:二进制浮点数无法精确表示许多十进制小数,累加后会出现肉眼意外的尾差。订单领域常用最小货币单位 long amountMinor 加 ISO 货币代码,或在确需十进制运算时使用 BigDecimal 和明确的 RoundingMode。Android 资源字符串负责本地化的文案与参数位置,例如 getString(R.string.order_total, formattedAmount);它不替代金额计算,也不应在领域层持有 Context。

Android 传输边界:谁带数据,谁恢复语义

#mermaid-svg-fcqnmWPQdKNqG0ck{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fcqnmWPQdKNqG0ck .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fcqnmWPQdKNqG0ck .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fcqnmWPQdKNqG0ck .error-icon{fill:#552222;}#mermaid-svg-fcqnmWPQdKNqG0ck .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fcqnmWPQdKNqG0ck .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fcqnmWPQdKNqG0ck .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fcqnmWPQdKNqG0ck .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fcqnmWPQdKNqG0ck .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fcqnmWPQdKNqG0ck .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fcqnmWPQdKNqG0ck .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fcqnmWPQdKNqG0ck .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fcqnmWPQdKNqG0ck .marker.cross{stroke:#333333;}#mermaid-svg-fcqnmWPQdKNqG0ck svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fcqnmWPQdKNqG0ck p{margin:0;}#mermaid-svg-fcqnmWPQdKNqG0ck .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fcqnmWPQdKNqG0ck .cluster-label text{fill:#333;}#mermaid-svg-fcqnmWPQdKNqG0ck .cluster-label span{color:#333;}#mermaid-svg-fcqnmWPQdKNqG0ck .cluster-label span p{background-color:transparent;}#mermaid-svg-fcqnmWPQdKNqG0ck .label text,#mermaid-svg-fcqnmWPQdKNqG0ck span{fill:#333;color:#333;}#mermaid-svg-fcqnmWPQdKNqG0ck .node rect,#mermaid-svg-fcqnmWPQdKNqG0ck .node circle,#mermaid-svg-fcqnmWPQdKNqG0ck .node ellipse,#mermaid-svg-fcqnmWPQdKNqG0ck .node polygon,#mermaid-svg-fcqnmWPQdKNqG0ck .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fcqnmWPQdKNqG0ck .rough-node .label text,#mermaid-svg-fcqnmWPQdKNqG0ck .node .label text,#mermaid-svg-fcqnmWPQdKNqG0ck .image-shape .label,#mermaid-svg-fcqnmWPQdKNqG0ck .icon-shape .label{text-anchor:middle;}#mermaid-svg-fcqnmWPQdKNqG0ck .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fcqnmWPQdKNqG0ck .rough-node .label,#mermaid-svg-fcqnmWPQdKNqG0ck .node .label,#mermaid-svg-fcqnmWPQdKNqG0ck .image-shape .label,#mermaid-svg-fcqnmWPQdKNqG0ck .icon-shape .label{text-align:center;}#mermaid-svg-fcqnmWPQdKNqG0ck .node.clickable{cursor:pointer;}#mermaid-svg-fcqnmWPQdKNqG0ck .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fcqnmWPQdKNqG0ck .arrowheadPath{fill:#333333;}#mermaid-svg-fcqnmWPQdKNqG0ck .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fcqnmWPQdKNqG0ck .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fcqnmWPQdKNqG0ck .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fcqnmWPQdKNqG0ck .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fcqnmWPQdKNqG0ck .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fcqnmWPQdKNqG0ck .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fcqnmWPQdKNqG0ck .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fcqnmWPQdKNqG0ck .cluster text{fill:#333;}#mermaid-svg-fcqnmWPQdKNqG0ck .cluster span{color:#333;}#mermaid-svg-fcqnmWPQdKNqG0ck div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fcqnmWPQdKNqG0ck .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fcqnmWPQdKNqG0ck rect.text{fill:none;stroke-width:0;}#mermaid-svg-fcqnmWPQdKNqG0ck .icon-shape,#mermaid-svg-fcqnmWPQdKNqG0ck .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fcqnmWPQdKNqG0ck .icon-shape p,#mermaid-svg-fcqnmWPQdKNqG0ck .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fcqnmWPQdKNqG0ck .icon-shape .label rect,#mermaid-svg-fcqnmWPQdKNqG0ck .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fcqnmWPQdKNqG0ck .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fcqnmWPQdKNqG0ck .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fcqnmWPQdKNqG0ck :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 订单详情页
OrderArgs Mapper
Intent Extras
Bundle
物流页面
参数校验
订单仓库

Intent 描述一次组件启动请求,extras 是其附带参数;Bundle 是按键保存多种受支持值的容器。它们适合传 orderId、筛选条件、少量显示状态或可恢复的轻量值。到达目标页面后,页面应先校验参数,再用 orderId 让仓库读取当前订单,而不是相信旧页面塞入的整个可变订单。

Parcelable 是 Android 为高效进程间或组件间编组提供的接口,Parcel 是有序的读写缓冲;写入和读取字段的顺序及类型必须一致。它适合短生命周期、结构明确且体积小的传输快照。它并不提供长期持久化、版本迁移、加密或业务校验。Serializable 使用反射式 Java 序列化,常常分配更多对象且不适合 Android 热路径;不要因"实现一个接口很省事"就把它作为默认方案。
#mermaid-svg-FKdIVXWFjJOaSCrp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FKdIVXWFjJOaSCrp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FKdIVXWFjJOaSCrp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FKdIVXWFjJOaSCrp .error-icon{fill:#552222;}#mermaid-svg-FKdIVXWFjJOaSCrp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FKdIVXWFjJOaSCrp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FKdIVXWFjJOaSCrp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FKdIVXWFjJOaSCrp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FKdIVXWFjJOaSCrp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FKdIVXWFjJOaSCrp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FKdIVXWFjJOaSCrp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FKdIVXWFjJOaSCrp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FKdIVXWFjJOaSCrp .marker.cross{stroke:#333333;}#mermaid-svg-FKdIVXWFjJOaSCrp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FKdIVXWFjJOaSCrp p{margin:0;}#mermaid-svg-FKdIVXWFjJOaSCrp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FKdIVXWFjJOaSCrp .cluster-label text{fill:#333;}#mermaid-svg-FKdIVXWFjJOaSCrp .cluster-label span{color:#333;}#mermaid-svg-FKdIVXWFjJOaSCrp .cluster-label span p{background-color:transparent;}#mermaid-svg-FKdIVXWFjJOaSCrp .label text,#mermaid-svg-FKdIVXWFjJOaSCrp span{fill:#333;color:#333;}#mermaid-svg-FKdIVXWFjJOaSCrp .node rect,#mermaid-svg-FKdIVXWFjJOaSCrp .node circle,#mermaid-svg-FKdIVXWFjJOaSCrp .node ellipse,#mermaid-svg-FKdIVXWFjJOaSCrp .node polygon,#mermaid-svg-FKdIVXWFjJOaSCrp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FKdIVXWFjJOaSCrp .rough-node .label text,#mermaid-svg-FKdIVXWFjJOaSCrp .node .label text,#mermaid-svg-FKdIVXWFjJOaSCrp .image-shape .label,#mermaid-svg-FKdIVXWFjJOaSCrp .icon-shape .label{text-anchor:middle;}#mermaid-svg-FKdIVXWFjJOaSCrp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FKdIVXWFjJOaSCrp .rough-node .label,#mermaid-svg-FKdIVXWFjJOaSCrp .node .label,#mermaid-svg-FKdIVXWFjJOaSCrp .image-shape .label,#mermaid-svg-FKdIVXWFjJOaSCrp .icon-shape .label{text-align:center;}#mermaid-svg-FKdIVXWFjJOaSCrp .node.clickable{cursor:pointer;}#mermaid-svg-FKdIVXWFjJOaSCrp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FKdIVXWFjJOaSCrp .arrowheadPath{fill:#333333;}#mermaid-svg-FKdIVXWFjJOaSCrp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FKdIVXWFjJOaSCrp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FKdIVXWFjJOaSCrp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FKdIVXWFjJOaSCrp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FKdIVXWFjJOaSCrp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FKdIVXWFjJOaSCrp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FKdIVXWFjJOaSCrp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FKdIVXWFjJOaSCrp .cluster text{fill:#333;}#mermaid-svg-FKdIVXWFjJOaSCrp .cluster span{color:#333;}#mermaid-svg-FKdIVXWFjJOaSCrp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FKdIVXWFjJOaSCrp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FKdIVXWFjJOaSCrp rect.text{fill:none;stroke-width:0;}#mermaid-svg-FKdIVXWFjJOaSCrp .icon-shape,#mermaid-svg-FKdIVXWFjJOaSCrp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FKdIVXWFjJOaSCrp .icon-shape p,#mermaid-svg-FKdIVXWFjJOaSCrp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FKdIVXWFjJOaSCrp .icon-shape .label rect,#mermaid-svg-FKdIVXWFjJOaSCrp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FKdIVXWFjJOaSCrp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FKdIVXWFjJOaSCrp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FKdIVXWFjJOaSCrp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Immutable Order
Parcelable Snapshot
Screen Launch
Saved State Bundle
Process Stopped
Screen Recreated
Restore Order Id
Repository Source

这里的关键是边界:配置变更、导航返回栈和系统保存状态都可能让页面重新创建;进程死亡后,普通 Java 对象及其内存引用不再存在。Bundle 中保留下来的只是可编组的数据,不是原对象的连续生命。恢复路径至少要处理字段缺失、版本变化、订单已取消或数据源不可用,并把最终业务真相交给仓库或后端。

源码锚点与责任表

锚点 层次与结论 类型 谁创建、谁持有、何时交付
Intent 组件启动请求可携带 extras 源码事实 调用页面创建;系统根据 Intent 启动目标组件;目标组件从 Intent 读取值
Bundle 以 key 保存受支持编组值的容器 源码事实 调用方或系统状态保存创建;Intent 或保存状态持有;恢复时读出
Parcelable 与 Parcel 类型定义跨 Android 边界的写入与读取协议 源码事实 类型实现协议;Framework 或 Binder 在需要编组时使用;接收端重建快照
String、BigDecimal、Instant 值的含义由领域层定义,格式化不是存储 源码事实加概念边界 领域工厂创建;不可变对象由订单快照持有;展示层转换为文案
OrderArgs 只传订单 ID 和必要导航参数 简化伪代码 页面映射后写入 extras;目标页校验并交给仓库
OrderRepository 以 ID 重新读取权威订单 生产建议 应用注入或页面范围创建;数据层持有;恢复和刷新时返回最新状态
SavedStateHandle 或数据库 保存轻量恢复键、持久业务事实 生产建议 架构层管理;不要把完整大对象塞入 Intent;在重建后恢复

表中的 Bundle、Parcel 和 Parcelable 是 Android Framework 可追踪的职责锚点;OrderArgs 是教学命名,不是系统类。生产代码还应让网络 DTO、数据库实体、领域订单和 UI 展示模型各自负责自己的版本演进。

紧凑订单模型:状态、金额、时间都可比较

下面是简化 Java 伪代码 。它展示不可变快照、稳定状态 ID、最小货币单位和显示格式化的分层;代码省略了 Android 的 Parcelable.Creator 样板和实际资源文件。

java 复制代码
enum OrderState {
    PENDING_PAYMENT("pending_payment"),
    PAID("paid"),
    CLOSED("closed");

    final String stableId;

    OrderState(String stableId) {
        this.stableId = stableId;
    }

    static OrderState fromStableId(String value) {
        for (OrderState state : values()) {
            if (state.stableId.equals(value)) return state;
        }
        throw new IllegalArgumentException("Unknown order state");
    }
}

final class Order {
    final String id;
    final OrderState state;
    final long amountMinor;
    final String currencyCode;
    final Instant paidAt;

    Order(String id, OrderState state, long amountMinor,
          String currencyCode, Instant paidAt) {
        if (id == null || id.isEmpty()) throw new IllegalArgumentException("id");
        if (amountMinor < 0) throw new IllegalArgumentException("amountMinor");
        this.id = id;
        this.state = state;
        this.amountMinor = amountMinor;
        this.currencyCode = currencyCode;
        this.paidAt = paidAt;
    }

    String displayTotal(Locale locale) {
        Currency currency = Currency.getInstance(currencyCode);
        BigDecimal major = BigDecimal.valueOf(
            amountMinor, currency.getDefaultFractionDigits());
        NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
        formatter.setCurrency(currency);
        return formatter.format(major);
    }

    String displayPaidAt(ZoneId zone, Locale locale) {
        if (paidAt == null) return "未支付";
        return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm", locale)
            .withZone(zone)
            .format(paidAt);
    }
}

这里的 locale 是调用方显式传入的展示 locale,例如用户资料或应用语言设置;它决定数字分组、小数点、货币符号位置和日期文字的本地化规则,并不决定订单使用哪一种货币。currencyCode 通过 Currency.getInstance 决定格式化的货币单位;只有调用方特意传入设备默认 locale 时,展示习惯才随设备默认设置变化。领域层仍保存货币代码和最小单位,而不保存已经格式化的金额文本。

final 字段与不暴露 setter 让这个对象在构造后不再改变,适合比较、缓存和 UI 渲染;但不可变不等于自动正确。equals 与 hashCode 必须一起重写,且要先定义身份语义:订单是否仅以 id 相等,还是同一 id 且版本或全部字段相等。用于 RecyclerView 差分时,通常分别比较"是否同一订单"和"内容是否相同",不要把 Java 默认引用相等误认成业务相等。

**刻意省略:**示例没有处理货币小数位因币种而异、税费和优惠拆分、BigDecimal 运算时的舍入策略、状态迁移授权、网络并发更新、序列化版本、国际化资源字符串、数据库事务及 Parcelable 字段兼容。生产系统应由服务端或持久化层定义权威版本,客户端只根据协议展示和请求操作。

让传输模型比领域模型更小

跨页面时可以显式定义一个只含恢复所需字段的快照。下面也是简化伪代码 :它传递 orderId 和一个可选来源标记,目标页面不把这个 Bundle 保存为自己的领域状态。

java 复制代码
final class OrderArgs {
    static final String KEY_ORDER_ID = "order_id";
    static final String KEY_SOURCE = "source";

    static Intent toIntent(Context context, String orderId, String source) {
        return new Intent(context, ShippingActivity.class)
            .putExtra(KEY_ORDER_ID, orderId)
            .putExtra(KEY_SOURCE, source);
    }

    static String requireOrderId(Intent intent) {
        String orderId = intent.getStringExtra(KEY_ORDER_ID);
        if (orderId == null || orderId.isEmpty()) {
            throw new IllegalArgumentException("Missing order_id");
        }
        return orderId;
    }
}

生产建议: 目标页把 requireOrderId 的失败转换为可诊断的导航错误或安全返回;随后由 ViewModel 和仓库加载订单。需要通过 savedInstanceState 或 SavedStateHandle 恢复时,保存 ID、筛选条件和小型 UI 状态即可。图片、长列表、数据库对象、文件内容和完整网络响应应以 ID、URI 或缓存键传递,避免把 Binder transaction 做成隐形数据管道。

五个会让订单不可靠的坑

  1. 用 double 表示金额。 浮点误差会在累加、折扣和比较时放大;使用最小货币单位 long,或使用带明确精度与舍入规则的 BigDecimal。
  2. 把可变 Bundle 当领域模型。 Bundle 是传输容器,键可以漏、类型可以错、生命周期可以结束;用不可变 Order 表示业务事实,用 mapper 隔开两层。
  3. 持久化 enum.name()。 枚举重命名会改变恢复数据;落盘和协议使用稳定 ID,并对未知值制定兼容策略。
  4. 在 Intent extras 里放大对象。 大对象会增加编组成本并可能触及 Binder 事务大小限制;传 ID、URI 或缓存键,再从数据源读取。
  5. 习惯性使用 Serializable。 它不是 Android 页面传参的默认选择;短小明确的快照优先 Parcelable,真正需要恢复的业务事实优先 ID 加持久数据源。

动手练习:跨组件安全地恢复订单

  1. 定义 OrderState 的稳定 ID,并写一个覆盖未知 ID 的测试或错误分支。
  2. 用 long amountMinor 和 Instant paidAt 构造不可变 Order;为订单身份和内容分别写比较方法或差分逻辑。
  3. 从详情页只把 orderId 与一个来源标记写入 Intent。让物流页校验字段,然后通过仓库按 ID 加载订单。
  4. 在 onSaveInstanceState 或 SavedStateHandle 保存 orderId,模拟清除进程后重新进入页面;验证页面会重新查询数据,而非依赖旧对象引用。
  5. 使用资源字符串接收已经格式化的金额和时间,切换设备语言与时区,确认领域数据没有被展示文案污染。

验收点是:删除任意页面内存中的 Order 引用后,只要订单 ID 和数据源仍有效,重建页面仍能显示正确状态;当恢复参数缺失、未知或过期时,页面不会把它伪装成一个有效订单。

检查表、面试表达与下一站

检查表: 订单 ID 是否稳定且已校验?金额是否避免 double?状态是否使用稳定存储 ID?领域对象是否不可变并定义了正确的相等语义?Intent 是否只传轻量参数?进程重建后是否从仓库恢复?金额和时间是否仅在展示层用资源与 locale 格式化?

面试表达: "我把 Intent、Bundle 和 Parcelable 看作 Android 组件边界的传输协议,不把它们当领域模型。订单用不可变值对象保存稳定 ID、状态、最小货币单位和绝对时间;状态枚举落盘用稳定 ID,金额避免 double。页面恢复只保存订单 ID,再由仓库获取权威数据,这样配置变化和进程死亡都不会把旧内存当成业务真相。"

下一篇进入集合与泛型:当单个 Order 的状态和相等语义清楚后,才适合讨论如何用 List、Map、缓存与泛型把多个订单安全地组织、比较和渲染。

相关推荐
Dovis(誓平步青云)5 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码6 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程7 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程10 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程11 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程11 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓12 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc12312 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium