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

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

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

先划清版本与事实边界

本文以 Java 8 时间 API 和当前 Android Framework 的公开 IntentBundleParcelParcelable 设计为概念参照。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.putExtraBundle.putString 传递的是值内容,不能把它们当作共享、可观察的领域对象。

Java 包装类如 IntegerLongBoolean 让值能表示为对象,并允许空值;但自动装箱并没有消除空指针风险: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 或保存状态持有;恢复时读出
ParcelableParcel 类型定义跨 Android 边界的写入与读取协议 源码事实 类型实现协议;Framework 或 Binder 在需要编组时使用;接收端重建快照
StringBigDecimalInstant 值的含义由领域层定义,格式化不是存储 源码事实加概念边界 领域工厂创建;不可变对象由订单快照持有;展示层转换为文案
OrderArgs 只传订单 ID 和必要导航参数 简化伪代码 页面映射后写入 extras;目标页校验并交给仓库
OrderRepository 以 ID 重新读取权威订单 生产建议 应用注入或页面范围创建;数据层持有;恢复和刷新时返回最新状态
SavedStateHandle 或数据库 保存轻量恢复键、持久业务事实 生产建议 架构层管理;不要把完整大对象塞入 Intent;在重建后恢复

表中的 BundleParcelParcelable 是 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 渲染;但不可变不等于自动正确。equalshashCode 必须一起重写,且要先定义身份语义:订单是否仅以 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 和仓库加载订单。需要通过 savedInstanceStateSavedStateHandle 恢复时,保存 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 amountMinorInstant paidAt 构造不可变 Order;为订单身份和内容分别写比较方法或差分逻辑。
  3. 从详情页只把 orderId 与一个来源标记写入 Intent。让物流页校验字段,然后通过仓库按 ID 加载订单。
  4. onSaveInstanceStateSavedStateHandle 保存 orderId,模拟清除进程后重新进入页面;验证页面会重新查询数据,而非依赖旧对象引用。
  5. 使用资源字符串接收已经格式化的金额和时间,切换设备语言与时区,确认领域数据没有被展示文案污染。

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

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

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

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

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

相关推荐
Railshiqian1 小时前
android16单手模式代码走读
android·多屏·单手模式
程序员-珍2 小时前
Android studio 突然打不开
android·java
郑州光合科技余经理10 小时前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
andyweike15 小时前
Android Automotive
android
又见情义16 小时前
记一次 RK3568 Android 13 蓝牙反复重启问题的排查与修复
android
hai_android16 小时前
Kotlin Flow combine 源码剖析:一个 Channel 如何优雅合并多条流
android·java·kotlin
敲代码的瓦龙17 小时前
Jetpack?ViewModel!!!
android·开发语言·kotlin·android-studio
Godikov18 小时前
Android 工控终端实战:从秒级卡顿到毫秒响应,SQLite/LitePal 性能优化全记录
android
星间都市山脉20 小时前
Android16 SystemService.onBootPhase 调用时机
android·java·linux·windows·ubuntu