# 用 Compose 构建可恢复的移动表单

原文: https://yishto.com/learn/application-platforms/compose-offline-checklist/

作者: [Yishto](https://yishto.com/about/)

为设备点检表建立草稿、校验、持久化和同步冲突状态，理解Compose重组与Android进程恢复的边界。

首次发布: 2026-11-10
内容更新: 2026-11-10

设备点检表包含设备编号、读数和异常说明。填写过程中旋转屏幕、离开页面或断网，都不应悄悄丢掉已经保存的输入。本课把一个合成点检单作为案例，设计 Compose 界面与本地数据之间的关系。

示例设备只是教学标识，不连接实际生产设备。首个工程记录草稿与同步状态，不执行控制命令。这样可以集中检查输入恢复、校验和服务端版本冲突。

## 草稿、表单状态和服务器版本

一张草稿包含稳定 ID、设备 ID、字段值、本地修改序号、最近确认的服务端版本，以及是否有待同步修改。记录字段时保留单位；“读数 12”缺少单位就不能可靠比较。异常说明允许为空还是必填，需要由明确规则决定，而不是让页面自行猜测。

表单状态另外描述正在保存、已保存、保存失败、等待同步和冲突。已保存表示写入本地成功，已同步表示服务端确认，两个提示不能混用。输入校验错误关联具体字段，用户修正一个字段时不必清空其他输入。

草稿是本地权威数据，界面由它派生。服务器返回的旧版本不能自动覆盖未同步输入。同步请求携带已知服务端版本，由服务端执行条件更新；版本不匹配时返回冲突，保留双方内容供用户比较。

## Compose 重组与状态所有权

Compose 根据观察到的状态重新计算界面。可组合函数可能反复执行，不应在函数体里无条件写数据库或发网络请求。输入组件接收值与事件回调，事件进入状态所有者，更新后由界面显示，这种状态提升让数据变化路径可追踪。

`remember` 保存组合期间的状态，不能保证进程终止后的恢复。`rememberSaveable` 适合框架可保存的小型 UI 状态，也不应装下完整业务记录或大量附件。用 ViewModel 组织页面状态时仍需要仓库和持久存储；ViewModel 本身不等于跨进程数据库。

旋转造成的配置变化与进程死亡是两个测试项目。恢复一个展开区块和恢复一份已保存点检单也有不同价值。先将业务字段写进本地存储，再根据目标生命周期决定焦点、滚动位置等短期状态怎样恢复。

## 建立 Android 工程与组件职责

在 Android Studio 创建 Compose 工程，确认 SDK、最低版本和依赖兼容。先建立点检数据类型、字段校验器与内存仓库，用合成设备展示输入框、异常说明、保存提示和提交按钮。标签要表达单位，错误文本可被辅助功能读取。

接着用本地数据库实现仓库，Room 可以作为工程选择，具体依赖与接口按当前官方文档配置。数据库记录业务字段与同步标记。ViewModel 读取仓库状态并暴露给页面，页面事件调用保存流程；阻塞数据库与网络工作不能直接放在 UI 回调中执行。

先打通“编辑、保存、离开、重开”再接服务器。每次持久化完成后才确认已保存；失败时保留当前输入并提供再次保存。启动加载现有草稿，查不到时创建空表，而不是把读取失败当作不存在并覆盖旧草稿。

## 离线写入与同步队列

本地保存和加入待同步操作应按同一个一致性边界处理。否则可能出现草稿已更新但没有队列任务，或任务存在却引用旧内容。队列项记录操作 ID、草稿 ID、基准版本和尝试状态，让进程重启后能重新枚举待办。

用户点击提交后先明确产品语义：它可以表示本地进入待同步状态，只有收到服务器确认才显示提交成功。网络不可用时保留任务；认证失败要求重新取得会话；字段被服务器拒绝时回到可修改状态。重复点击不能制造无界任务。

后台同步采用目标 Android 版本支持的调度机制，受网络和系统资源约束。不要承诺应用退出后按任意秒数完成。重新打开时也要主动检查队列，从持久状态恢复，而不是依赖某个仍存活的协程。

## 用小模型观察冲突保留

下面 Python 代码比较本地修订号与传入修订号，并在脏草稿遇到不同版本时拒绝覆盖。它只是冲突分支的离线模型，不包含 Kotlin、Compose、Room 或服务端条件更新。

```python
local = {"revision": 2, "text": "local edit", "dirty": True}
remote = {"revision": 1, "text": "older"}
def merge(draft, incoming):
    if draft["dirty"] and incoming["revision"] != draft["revision"]:
        return "conflict"
    draft.update(incoming, dirty=False)
    return "synced"
assert merge(local, remote) == "conflict"
assert local["text"] == "local edit"
print(local)
```

实际输出 `{'revision': 2, 'text': 'local edit', 'dirty': True}`。较旧的 `remote` 没有替换本地输入。代码将 `revision` 简化成单一比较字段；真实协议应分别记录本地修改序号与服务器基准版本，不能拿不同体系的序号直接排序。

冲突界面至少展示本地内容、服务器内容与选择动作。用户选择合并后生成新的本地修改，以当前服务器版本为基准再次提交。未经确认的覆盖应被禁止；“最新时间优先”也需要讨论设备时钟偏差和字段意义，不能默认为可靠规则。

## 恢复路径与重复操作

测试保存时终止进程，检查数据库中是完整旧记录还是完整新记录，避免部分字段被写入。重启时恢复最近成功保存的草稿，并说明尚未完成的保存。对附件，可以先完成文件写入和校验，再让记录引用它，防止恢复出不存在的文件。

同步超时只说明没有及时收到结果，不证明服务器未执行。重试应沿用操作 ID，服务器需提供幂等处理。接收到成功响应也要确认响应对应当前待办，不能让旧响应把用户之后的编辑标为已同步。

输入控件要允许合法的编辑中间态。例如负号或尚未完成的小数不能立刻改成零；提交校验再判断完整数值，并保留原输入便于修正。

## 练习：验收可恢复的点检单

建立两张草稿，设置读数范围和异常说明规则。分别制造本地写入失败、网络断开、服务器版本变化和重复提交。失败后字段值应保留，重试不会增加重复业务记录，冲突不会自动覆盖本地修改。

在模拟器测试旋转和进程终止，再记录目标设备上的离线恢复。验收时核对三处：页面字段、数据库草稿、服务器确认版本。它们可以短暂不同，但状态提示必须解释差异；只有确认的版本才进入已同步。本文模型的断言通过不能替代这套 Android 工程验收。

## 资料来源与延伸阅读

- [Compose state and architecture](https://developer.android.com/develop/ui/compose/state)
- [Build an offline-first app](https://developer.android.com/topic/architecture/data-layer/offline-first)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [页面、路由和状态应该放在哪里](https://yishto.com/learn/web-interface/state-routing-and-frontend-architecture/)
- [让数据库约束参与业务正确性](https://yishto.com/learn/service-data/database-indexes-and-transactions/)
