2025年3月30日·阅读约1分钟

Kotlin WorkManager 用于现场应用的后台同步模式

Kotlin WorkManager 后台同步模式,适用于现场应用:选择合适的工作类型、设置约束、使用指数回退,并展示用户可见的进度。

Kotlin WorkManager 用于现场应用的后台同步模式

对现场与运维类应用来说,可靠的后台同步意味着什么

在现场与运维类应用里,同步并不是“可有可无”的功能。它决定了设备上的工作何时对团队可见并变成真实的进度。当同步失败时,用户会很快发现:已完成的工单仍显示为“待处理”,照片丢失,或同一个报告被上传两次产生重复记录。

这些应用比典型的消费类应用更难做到健壮,因为手机经常处于非常糟糕的环境。网络在 LTE、弱 Wi‑Fi 与无信号之间切换。省电模式会阻止后台任务,应用被系统杀掉,系统更新或设备在路途中重启。一个可靠的 WorkManager 配置需要能在这些情况下一路存活而不出大问题。

可靠通常意味着四件事:

  • 最终一致(Eventually consistent):数据可能延迟到达,但最终会到达且不需要人工持续照看。\n- 可恢复(Recoverable):若上传过程被中断,下次运行能安全继续。\n- 可观察(Observable):用户和支持人员可以看出进展和卡住的位置。\n- 非破坏(Non-destructive):重试不会产生重复或破坏状态。

“立即运行”(Run now)适合用户触发的小操作并应尽快完成(例如用户在关闭工单前发送一次状态更新)。“等待”(Wait)适合更重的工作,如照片上传、批量更新,或任何在弱网络/省电模式下容易失败的任务。

示例:一名检查员在地下室没有信号的情况下提交了包含 12 张照片的表单。可靠的同步会先将所有内容保存在本地,标记为已排队,并在设备恢复真实网络时上传,且无需检查员重复操作。

选择合适的 WorkManager 构建块

先从最小且清晰的工作单元开始。这一决定对可靠性的影响往往比之后任何巧妙的重试逻辑都大。

一次性任务 vs 周期性任务

对因某个事件而需要发生的工作使用 OneTimeWorkRequest:例如新表单被保存、照片压缩完成或用户点击同步。立刻按需入队(并设置约束),让 WorkManager 在设备准备好时运行它。

对稳定维护任务使用 PeriodicWorkRequest,比如“检查更新”或夜间清理。周期性任务并不精确:它有最小间隔且可能依据电量和系统策略发生漂移,因此不应成为重要上传的唯一途径。

一个实用的模式是:将“一次性任务”用于“必须尽快同步”的场景,同时把周期性任务当作安全网。

选择 Worker、CoroutineWorker 还是 RxWorker

如果你用 Kotlin 并且使用 suspend 函数,优先使用 CoroutineWorker,它让代码更短并且取消行为更可预测。

Worker 适用于简单的阻塞代码,但要小心不要阻塞太久。

只有当应用已经大量使用 RxJava 时,RxWorker 才有意义,否则它只是额外的复杂度。

链式步骤还是一体化阶段?

当各步骤可以独立成功或失败且你希望单独重试并记录更清晰的日志时,链式很好。若步骤共享数据并且必须像一个事务一样处理,则在一个 worker 内分阶段更合适。

一个简单规则:

  • 当步骤有不同约束时采用链式(比如先在 Wi‑Fi 上传,再在任意网络下做小的 API 调用)。\n- 当你需要“全有或全无”的同步时,在一个 worker 里实现多个阶段。

WorkManager 保证工作会被持久化、能在进程死亡与重启后继续,并尊重约束。它不保证精确时机、立即执行,或在用户强制停止应用后继续运行。如果你在构建 Android 现场应用(包括由 AppMaster 生成的 Kotlin 应用),请把同步设计成能容忍延迟且让延迟变成可预期的一部分。

使同步安全:幂等、增量与可恢复

现场应用会重跑任务。手机会丢失信号,系统会杀进程,用户会因为看不到反馈而重复点击同步。如果你的后台同步不能安全重复执行,就会出现重复记录、丢失更新或无休止的重试。

从让每个服务端调用都能跑两次且结果一致开始。最简单的方法是为每个项目使用幂等键(例如与本地记录一起存储的 UUID),服务器将其视为“同一请求、同一结果”。若无法修改服务器,使用稳定的自然键与 upsert 接口,或带上版本号让服务器拒绝过期更新。

明确跟踪本地状态,这样 worker 在崩溃后能安全恢复而无需猜测。一个简单的状态机通常足够:

  • queued\n- uploading\n- uploaded\n- needs-review\n- failed-temporary

保持增量同步。不要一次性“同步所有内容”,而是存储光标(如 lastSuccessfulTimestamp 或服务器发行的 token)。读取一小页变更、应用它们,只有在该批次被完全提交到本地后才推进光标。小批量(例如 20–100 条)可以减少超时、让进度更可见,并限制在中断后需要重复的工作量。

让上传也能续传。对于照片或大负载,持久化文件 URI 与上传元数据,只有在服务器确认后才把项标记为已上传。worker 重启时应从上次已知状态继续,而不是全部重新开始。

示例:技术员在地下有 12 份表单并附上 8 张照片。设备恢复网络后,worker 分批上传,每个表单都有幂等键,光标仅在每个批次成功后推进。如果应用在中途被杀,重跑 worker 会完成剩余的已排队项目而不会重复任何内容。

与真实设备条件匹配的约束

约束是防止后台同步耗电、消耗流量或在最糟糕时机失败的护栏。你要设置反映现场设备行为的约束,而不是在你办公桌上测试时看到的那种理想条件。

从一套能保护用户但仍允许任务大多数时间运行的最小约束开始。一个实用的基线是:要求网络连接、避免在电量低时运行、并避免在存储极低时运行。仅当工作非常耗时且不敏感于时间时才要求“充电中”,因为很多现场设备在班次期间很少插电。

过度约束是“同步从不运行”的常见原因。如果你同时要求非计费网络、充电中且电量充足,你基本上在等一个几乎不会出现的完美时刻。如果业务今天就需要数据,比起等待理想条件,更好的办法是更频繁地运行更小的工作。

另一个现实问题是捕获门户(captive portals):手机显示已连接,但需要用户在酒店或公共 Wi‑Fi 页面上点击“接受”。WorkManager 无法可靠检测这种状态。把它当作普通失败处理:尝试同步、快速超时、稍后重试。同时在能检测到时在应用内显示“已连接到 Wi‑Fi 但无法访问互联网”之类的简单提示。

对小上传和大上传使用不同约束以保持应用响应:

  • 小负载(状态上报、表单元数据):任意网络,电池不低即可。\n- 大负载(照片、视频、地图包):尽可能要求非计费网络,并考虑要求充电。

示例:技术员保存了一个带 2 张照片的表单。可以在任意连接上提交表单字段,但把照片上传排队等待 Wi‑Fi 或更合适的时机。办公室能快速看到工单,而设备不会在后台用移动流量上传图片。

不惹恼用户的指数回退重试

为运维提供清晰的管理视图
为运维团队添加一个 Web 管理面板,让他们能看到哪些数据已同步、哪些失败以及原因。
构建管理端

重试是让现场应用看起来“稳如泰山”或“崩溃感十足”的关键。选择与预期失败类型相匹配的回退策略。

指数回退通常是网络场景下最安全的默认值。它会快速增加等待时间,避免在覆盖差时反复轰击服务器或消耗电量。线性回退适合短暂问题(例如不稳定的 VPN),但在弱信号区域往往重试太频繁。

基于失败类型做出重试决策,而不是仅仅基于“出错了”。一个简单规则集:

  • 网络超时、5xx、DNS、无连通性:返回 Result.retry()\n- 认证过期(401):尝试刷新 token 一次,然后失败并要求用户重新登录\n- 校验或 4xx(错误请求):返回 Result.failure() 并给出清晰的支持用错误信息\n- 冲突(409)且项已发送:若同步是幂等的,可将其视为成功

限制损害范围以防永久错误无限循环。设置最大尝试次数,超过后停止并展示一个安静且可执行的消息(不要重复推送通知)。

你也可以随着尝试次数增加调整行为。例如失败两次后,发送更小的批次或跳过大文件上传,直到下一次成功拉取为止。

val request = OneTimeWorkRequestBuilder<SyncWorker>()
  .setBackoffCriteria(
    BackoffPolicy.EXPONENTIAL,
    30, TimeUnit.SECONDS
  )
  .build()

// in doWork()
if (runAttemptCount >= 5) return Result.failure()
return Result.retry()

这样可以让重试更礼貌:唤醒更少、打扰更少,并在网络恢复时更快地恢复。

面向用户的进度:通知、前台工作与状态展示

一体化平台覆盖全栈
在一个平台上构建后端、Web 与原生移动应用,保持模型和认证一致。
创建应用

现场应用经常在用户意想不到的时候同步:在地下室、在慢网、在电量接近耗尽时。如果同步影响到用户正在等待的内容(上传、发送报告、照片批次),请让它可见且易于理解。短小且快速的静默后台工作很合适,但任何较长的工作都应该让用户感觉到诚实的进度。

何时需要前台执行

当任务运行时间长、时间敏感或明确与用户操作相关时,使用前台执行。在现代 Android 中,大文件上传如果不在前台运行,可能会被停止或延迟。在 WorkManager 中,这意味着返回一个 ForegroundInfo,系统会展示一个正在进行的通知。

一个好的通知要回答三件事:正在同步什么、进展到哪儿了以及如何停止。添加清晰的取消操作,让用户在使用计费流量或需要立即使用手机时可以中止操作。

让进度可信

进度应映射到真实单位,而不是模糊百分比。用 setProgress 更新进度,并在 UI(或状态页)通过 WorkInfo 读取它。

如果要上传 12 张照片和 3 份表单,就报告“已上传 5 / 共 15 项”,显示剩余项并保留最近的错误信息以便支持使用。

保持进度有意义:

  • 已完成项与剩余项\n- 当前阶段("上传照片"、"发送表单"、"最终确认")\n- 最后一次成功同步时间\n- 最近错误(简短、面向用户)\n- 可见的取消/停止选项

如果你的团队用 AppMaster 快速构建内部工具,遵守同样的规则:当用户能看到且与他们期望的结果一致时,他们会信任同步。

唯一任务、标签与避免重复同步作业

重复的同步任务会轻易耗电、烧流量并产生服务端冲突。WorkManager 提供两个简单工具来防止这种情况:唯一任务名和标签(tags)。

一个好的默认做法是把“sync”当作单条流水线。不要在每次应用唤醒时都入队新的任务,而是用同一个唯一任务名入队。这样,当用户打开应用、网络变化触发、周期性任务同时触发时,就不会出现同步风暴。

val request = OneTimeWorkRequestBuilder<SyncWorker>()
  .addTag("sync")
  .build()

WorkManager.getInstance(context)
  .enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, request)

选择策略是主要行为差异:

  • KEEP:如果已有同步在运行或排队,忽略新请求。对大多数“立即同步”按钮和自动触发器使用此策略。\n- REPLACE:取消当前的并重新开始。当输入确实发生变化(如用户切换账户或选择不同项目)时使用。

标签是你控制与可见性的把手。用像 sync 这样的稳定标签,你可以取消任务、查询状态或过滤日志,而无需追踪具体 ID。这对手动“立即同步”特别有用:你可以检查是否已有任务在运行,并显示清晰信息而不是再发起另一个 worker。

周期性和按需同步不应互相冲突。把它们分开但保持协调:

  • enqueueUniquePeriodicWork("sync_periodic", KEEP, ...) 做计划任务。\n- 用 enqueueUniqueWork("sync", KEEP, ...) 做按需任务。\n- 在 worker 中,如果没有可上传或下载的内容应迅速退出,这样周期性执行的成本就低。\n- 可选地,让周期性 worker 自身入队相同的一次性唯一同步任务,这样所有真实工作都在同一个地方发生。

这些模式让后台同步更可预测:一次只运行一个同步,容易取消,易于观察。

逐步示例:一个实用的后台同步流水线

部署到你的运行环境
部署到 AppMaster Cloud 或你们的 AWS、Azure、Google Cloud 环境中。
部署应用

把可靠的同步流水线当成一个小型状态机来构建更容易:工作项先在本地存在,WorkManager 只在条件满足时把它们推进到下一步。

一个可以交付的简单流水线

  1. 从本地“队列”表开始。存储让任务能恢复所需的最小元数据:项 id、类型(表单、照片、备注)、状态(pending、uploading、done)、尝试次数、最近错误,以及用于下载的光标或服务器修订号。\n
  2. 对于用户点击的“立即同步”,用与真实世界匹配的约束入队 OneTimeWorkRequest。常见选择是要求网络连接与电池不低。若上传量大,也要求充电。\n
  3. 实现一个 CoroutineWorker,明确分阶段:上传、下载、调和(reconcile)。保持每个阶段的增量性。只上传标记为 pending 的项,只下载上次光标之后的变更,随后用简单规则解决冲突(例如:服务器在指派字段上优先,客户端在本地草稿备注上优先)。\n
  4. 添加带回退的重试,但对重试的对象要有选择性。超时与 500 系列应重试;401(已登出)应快速失败并告知 UI 具体原因。\n
  5. 观察 WorkInfo 来驱动 UI 与通知。对“上传 3 / 10”这样的阶段使用进度更新,并展示简短的失败信息提示下一步操作(重试、登录、连接 Wi‑Fi)。
val constraints = Constraints.Builder()
  .setRequiredNetworkType(NetworkType.CONNECTED)
  .setRequiresBatteryNotLow(true)
  .build()

val request = OneTimeWorkRequestBuilder<SyncWorker>()
  .setConstraints(constraints)
  .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
  .build()

当你把队列保留在本地并让 worker 阶段明确时,你会得到可预测的行为:工作可以暂停、恢复,并向用户解释自己发生了什么,而不是让系统去猜测。

常见错误与陷阱(以及如何避免)

可靠的同步最常因为一些在测试时看上去无害的小选择而失败,在真实设备上就会崩盘。目标不是尽可能频繁地运行,而是在恰当的时候运行、做恰当的事,并在无法继续时干净地停止。

需要注意的陷阱

  • 在没有约束的情况下进行大文件上传。如果在任何网络与任何电量下上传照片或大负载,用户会很不满。为网络类型和低电量设置约束,并把大任务拆成更小块。\n- 永远重试所有错误。401、过期 token 或缺失权限不是临时问题。把它们标记为硬失败,展示清晰的可执行动作(重新登录),只对真正的短暂问题(如超时)重试。\n- 无意间创建重复项。如果 worker 会运行两次且请求不是幂等的,服务器会收到重复创建。为每项生成稳定的客户端 ID 并让服务器把重复当作更新而不是新行。\n- 用周期性任务做近实时需求。周期性任务适合维护,而非“立即同步”。对用户发起的同步应入队一次性唯一工作,让用户在需要时触发它。\n- 过早报告“100%”。上传完成不等于数据被接受并调和。按阶段跟踪进度(queued、uploading、server confirmed),只有在服务器确认后才显示已完成。

一个具体示例:技术员在电梯内用弱信号提交含三张照片的表单。如果你立刻在任何条件下开始上传,上传会卡住、重试激增,并且在应用重启后可能导致表单被创建两次。若你在可用网络下再开始上传、按步骤处理并为每表单使用稳定 ID,同样场景将以一条干净的服务器记录和真实的进度信息结束。

上线前的快速检查清单

生成可扩展的后端
生成可用于生产的 Go 后端,并在需求变化时保持同步契约稳定。
创建后端

上线前,请以真实现场用户可能破坏应用的方式测试同步:断断续续的信号、耗尽的电池和大量点击。开发手机上看起来没问题的实现,若在调度、重试或状态报告上有问题,在真实环境中仍会失败。

在至少一部旧设备和一部新设备上运行以下检查。保存日志,但也看用户界面中的展示。

  • 无网络后恢复:在断网情况下开始同步,然后恢复网络。确认工作被排队(而不是快速失败),并且恢复后能继续且不重复上传。\n- 设备重启:开始同步时重启设备,再打开应用。验证工作能继续或被重新调度,并且应用显示正确的当前状态(而不是卡在“正在同步”)。\n- 低电量与低存储:开启省电模式、尽可能降低电量并接近满存储。确认任务在应等待时等待,条件改善后继续,并且不会在重试循环中耗尽电量。\n- 重复触发:连续点击“同步”按钮或从多个界面触发同步。结果应仍然是一次逻辑同步,而不是多并行 worker 争抢同一记录。\n- 可解释的服务器失败:模拟 500、超时与认证错误。检查重试是否回退并在达到上限后停止,用户应看到类似“无法连接服务器,将会重试”的清晰消息而不是通用失败。

若任何测试让应用处于不清晰状态,请把它当成 Bug。用户会容忍慢,但不会容忍数据丢失或不知所措。

示例场景:离线表单与现场照片上传

无需重写即可测试同步逻辑
用可视化逻辑原型测试上传队列、重试和进度 UX,日后可继续完善。
立即试用

技术员到达现场信号很差。他们离线填写一份服务表单,拍了 12 张照片,并在离开前点击提交。应用先把所有内容保存在本地(例如本地数据库):一条表单记录和每张照片一条记录,带有清晰的状态如 PENDINGUPLOADINGDONEFAILED

点击提交时,应用以唯一同步任务的方式入队,避免用户重复点击时创建重复记录。常见做法是用 WorkManager 链式先上传照片(大且慢),照片确认后再发送表单负载。

同步仅在符合真实条件时运行。例如等待网络连接、电量不低且存储足够。如果技术员仍在地下室无信号,则不会在后台循环耗电。

进度应当明显且友好。上传作为前台工作运行并显示类似“已上传 3 / 12”的通知,包含明确的取消操作。若用户取消,应用停止当前工作并把剩余项保留在 PENDING,以便稍后重试而不丢失数据。

在不稳定热点情况下重试也应该有礼貌:第一次失败很快重试,但每次失败都会等待更久(指数回退)。最初感觉响应很快,然后逐渐减频以避免耗电与网络打扰。

对运维团队来说,收益是切实可见的:更少重复提交(因为项是幂等且唯一入队)、清晰的失败状态(哪个照片失败、原因以及何时会重试),以及用户对“已提交”意味着“已安全保存并会同步”的信任。

下一步:先交付可靠性,再逐步扩展同步范围

在增加更多同步功能之前,先弄清“完成”对你而言意味着什么。对大多数现场应用来说,完成并不是“请求已发送”,而是“服务器已接受并确认”,并且 UI 状态与现实一致。一个显示“已同步”的表单在应用重启后应继续保持该状态;失败的表单应明确告诉用户下一步该怎么做。

通过提供一小组可见信号来让应用更值得信任(并且支持团队能询问这些信号)。保持这些信号在不同屏幕间简单且一致:

  • 最后一次成功同步时间\n- 最近一次同步错误(简短信息,而不是堆栈跟踪)\n- 待处理项数量(例如:3 份表单,12 张照片)\n- 当前同步状态(Idle、Syncing、Needs attention)

把可观测性视为功能的一部分。当人在弱网络下不知道应用是否在工作时,这能节省大量时间。

如果你也在构建后端和管理工具,一起生成它们有助于保持同步契约稳定。AppMaster (appmaster.io) 能生成可用于生产的后端、Web 管理面板和原生移动应用,这能帮助你在专注于同步边界问题时保持模型与认证的一致性。

最后,先运行一个小规模试点。挑选一个端到端的同步切片(例如“提交带 1–2 张照片的检查表单”),并把约束、重试与面向用户的进度做得完整可靠。等这部分变得平淡且可预测后,再逐步扩展功能。

常见问题

在现场应用中,“可靠的后台同步”到底是什么意思?

可靠的后台同步意味着设备上创建的工作先保存在本地,之后会在后台上传,用户无需重复操作。它应能在应用被杀、重启、网络差以及重试时不丢失数据或产生重复记录。

我应该什么时候使用 OneTimeWorkRequest 而不是 PeriodicWorkRequest?

将 OneTimeWorkRequest 用于由真实事件触发的工作,例如“表单已保存”、“照片已添加”或用户点击同步。把 PeriodicWorkRequest 用作维护或保底手段,但不要把它当作重要上传的唯一途径,因为周期性任务的执行时间会有漂移。

我应该选择哪种 Worker:Worker、CoroutineWorker 还是 RxWorker?

如果你使用 Kotlin 且同步代码基于 suspend 函数,优先选用 CoroutineWorker,它让代码更简洁并且取消行为更可预测。Worker 适合短时间的阻塞任务,RxWorker 只有在你已大量使用 RxJava 时才值得考虑。

我应该把多个步骤串联成多个 worker 还是在一个 worker 里分阶段完成?

当各个步骤有不同约束且可独立重试时,使用链式 Worker,比如先在 Wi‑Fi 上传大文件、再在任意网络下调用轻量 API。若步骤共享状态并且要作为一个事务处理,则在一个 Worker 中分阶段完成会更好,保证“要么全部成功,要么都不算”。

我如何阻止重试在服务器上创建重复记录?

通过为每个项目使用幂等键(例如与本地记录一起存储的 UUID)来确保每次创建/更新请求可以安全地重复执行。如果不能修改服务器,尽量使用稳定的自然键和 upsert 接口,或者通过版本号让服务器拒绝过期更新。

如果应用在同步中途被杀,如何让上传可恢复?

把本地状态持久化,例如 queued、uploading、uploaded、failed,这样 worker 可以在重启后从上次状态继续。只有在服务器确认后才把项目标记为已完成,并存储足够的元数据(例如文件 URI 和尝试次数)以便在崩溃或重启后继续上传。

针对现场应用,同步任务的哪些约束是合适的默认设置?

为现场应用设定的默认约束应尽量保护用户同时又能让任务在大多数时间运行:要求网络连接、避免在低电量时运行并避免在存储极低时运行。对“非计费网络(unmetered)”和“充电中”要谨慎,因为这些要求会让很多现场设备几乎从未满足条件。

我的应用应如何处理验证门户或“连上 Wi‑Fi 但无网络”的情况?

把“连接但无互联网”当作普通失败处理:快速超时并返回 Result.retry(),以后重试。如果你在请求时能检测到这一点,在界面上显示简单提示(例如“已连接到 Wi‑Fi 但无法访问互联网”),让用户明白为何看上去在线但同步没有进展。

在网络不稳定时,最安全的重试策略是什么?

对于网络故障,使用指数回退(exponential backoff)通常是最稳妥的默认策略,这样在信号差时不会频繁轰击服务器或耗电。对超时和 5xx 错误重试,对永久性错误(如 4xx 验证失败)快速失败,并在达到最大重试次数后停止循环。

我如何防止“同步风暴”并同时展示用户可见的进度?

将同步作为唯一名称的任务入队,避免重复触发并行工作,同时为长期或用户触发的任务展示可信的进度。若工作是长时间运行或由用户发起,将其作为前台任务运行并显示持续通知,通知中显示真实计数并提供明确的取消选项。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧