订单确认页看上去只是把商品名、金额、状态和时间从一个页面带到另一个页面;真正的难点是,这些值会经过
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 做成隐形数据管道。
五个会让订单不可靠的坑
- 用
double表示金额。 浮点误差会在累加、折扣和比较时放大;使用最小货币单位long,或使用带明确精度与舍入规则的BigDecimal。 - 把可变
Bundle当领域模型。Bundle是传输容器,键可以漏、类型可以错、生命周期可以结束;用不可变Order表示业务事实,用 mapper 隔开两层。 - 持久化
enum.name()。 枚举重命名会改变恢复数据;落盘和协议使用稳定 ID,并对未知值制定兼容策略。 - 在 Intent extras 里放大对象。 大对象会增加编组成本并可能触及 Binder 事务大小限制;传 ID、URI 或缓存键,再从数据源读取。
- 习惯性使用
Serializable。 它不是 Android 页面传参的默认选择;短小明确的快照优先Parcelable,真正需要恢复的业务事实优先 ID 加持久数据源。
动手练习:跨组件安全地恢复订单
- 定义
OrderState的稳定 ID,并写一个覆盖未知 ID 的测试或错误分支。 - 用
long amountMinor和Instant paidAt构造不可变Order;为订单身份和内容分别写比较方法或差分逻辑。 - 从详情页只把
orderId与一个来源标记写入 Intent。让物流页校验字段,然后通过仓库按 ID 加载订单。 - 在
onSaveInstanceState或SavedStateHandle保存orderId,模拟清除进程后重新进入页面;验证页面会重新查询数据,而非依赖旧对象引用。 - 使用资源字符串接收已经格式化的金额和时间,切换设备语言与时区,确认领域数据没有被展示文案污染。
验收点是:删除任意页面内存中的 Order 引用后,只要订单 ID 和数据源仍有效,重建页面仍能显示正确状态;当恢复参数缺失、未知或过期时,页面不会把它伪装成一个有效订单。
检查表、面试表达与下一站
检查表: 订单 ID 是否稳定且已校验?金额是否避免 double?状态是否使用稳定存储 ID?领域对象是否不可变并定义了正确的相等语义?Intent 是否只传轻量参数?进程重建后是否从仓库恢复?金额和时间是否仅在展示层用资源与 locale 格式化?
面试表达: "我把 Intent、Bundle 和 Parcelable 看作 Android 组件边界的传输协议,不把它们当领域模型。订单用不可变值对象保存稳定 ID、状态、最小货币单位和绝对时间;状态枚举落盘用稳定 ID,金额避免 double。页面恢复只保存订单 ID,再由仓库获取权威数据,这样配置变化和进程死亡都不会把旧内存当成业务真相。"
下一篇进入集合与泛型:当单个 Order 的状态和相等语义清楚后,才适合讨论如何用 List、Map、缓存与泛型把多个订单安全地组织、比较和渲染。