用 Compose 构建可恢复的移动表单
为设备点检表建立草稿、校验、持久化和同步冲突状态,理解Compose重组与Android进程恢复的边界。
实现本地点检草稿与同步队列,分别测试旋转、进程终止、离线提交和版本冲突时的输入保留。
设备点检表包含设备编号、读数和异常说明。填写过程中旋转屏幕、离开页面或断网,都不应悄悄丢掉已经保存的输入。本课把一个合成点检单作为案例,设计 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 或服务端条件更新。
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 工程验收。
资料来源与延伸阅读
资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。