30 thg 3, 2025·8 phút đọc

Mẫu đồng bộ nền với Kotlin WorkManager cho ứng dụng hiện trường

Mẫu đồng bộ nền với Kotlin WorkManager cho ứng dụng hiện trường: chọn loại công việc phù hợp, đặt constraints, dùng backoff lũy thừa, và hiển thị tiến trình người dùng thấy được.

Mẫu đồng bộ nền với Kotlin WorkManager cho ứng dụng hiện trường

Đồng bộ nền đáng tin cậy nghĩa là gì với ứng dụng hiện trường và ops

Trong các ứng dụng hiện trường và vận hành, sync không phải là "nên có" mà là cách công việc rời khỏi thiết bị và trở nên thực cho cả đội. Khi sync thất bại, người dùng nhận ra rất nhanh: một công việc đã xong vẫn hiện là "đang chờ", ảnh biến mất, hoặc cùng một báo cáo tải lên hai lần tạo bản sao.

Những app này khó hơn so với app tiêu dùng vì điện thoại hoạt động trong điều kiện tệ nhất. Mạng chuyển giữa LTE, Wi‑Fi yếu và mất sóng. Chế độ tiết kiệm pin chặn công việc nền. App bị kill, OS cập nhật, thiết bị khởi động lại giữa chuyến đi. Một setup WorkManager đáng tin cậy cần sống sót qua tất cả điều đó mà không drama.

Đáng tin cậy thường có bốn ý nghĩa:

  • Cuối cùng nhất quán: dữ liệu có thể đến muộn, nhưng sẽ đến mà không cần can thiệp tay.\n- Có thể khôi phục: nếu app chết giữa chừng khi upload, lần chạy sau tiếp tục an toàn.\n- Có thể quan sát: người dùng và support biết điều gì đang xảy ra và cái gì bị kẹt.\n- Không phá hủy: thử lại không tạo trùng lặp hay làm hỏng trạng thái.

"Chạy ngay" phù hợp với hành động nhỏ do người dùng kích hoạt và nên hoàn thành sớm (ví dụ gửi một cập nhật trạng thái trước khi người dùng đóng công việc). "Đợi" phù hợp với công việc nặng hơn như upload ảnh, cập nhật theo lô, hoặc bất kỳ việc nào có thể ngốn pin hoặc dễ thất bại trên mạng kém.

Ví dụ: một kiểm tra viên nộp form kèm 12 ảnh trong tầng hầm không có sóng. Một sync đáng tin cậy lưu tất cả cục bộ, đánh dấu vào hàng đợi, và tải lên sau khi thiết bị có kết nối thực, mà không bắt kiểm tra viên làm lại.

Chọn đúng khối xây dựng của WorkManager

Bắt đầu bằng cách chọn đơn vị công việc nhỏ nhất, rõ ràng nhất. Quyết định đó ảnh hưởng đến độ tin cậy hơn bất kỳ logic thử lại thông minh nào sau này.

One-time vs periodic work

Dùng OneTimeWorkRequest cho công việc nên xảy ra vì có sự thay đổi: một form mới được lưu, một ảnh nén xong, hoặc người dùng bấm Sync. Enqueue ngay (kèm constraints) và để WorkManager chạy khi thiết bị sẵn sàng.

Dùng PeriodicWorkRequest cho bảo trì định kỳ, như "kiểm tra cập nhật" hoặc dọn dẹp ban đêm. Periodic work không chính xác từng giây: nó có khoảng thời gian tối thiểu và có thể trôi dựa trên chính sách pin và hệ thống, nên không nên là con đường duy nhất cho các upload quan trọng.

Một pattern thực dụng là dùng one-time cho "phải sync sớm", với periodic như mạng an toàn.

Chọn Worker, CoroutineWorker, hay RxWorker

Nếu bạn viết Kotlin và dùng suspend functions, ưu tiên CoroutineWorker. Nó giữ code ngắn và làm cho việc huỷ chạy đúng như bạn mong muốn.

Worker phù hợp code chặn đơn giản, nhưng cần cẩn thận không chặn quá lâu.

RxWorker chỉ hợp lý nếu app bạn đã dùng RxJava nặng; nếu không thì nó là sự phức tạp thừa.

Chain các bước hay chạy một worker theo pha?

Chaining tuyệt khi các bước có thể thành công hoặc thất bại độc lập, và bạn muốn thử lại riêng từng bước cùng nhật ký rõ ràng. Một worker với các pha có thể tốt hơn khi các bước chia sẻ dữ liệu và phải được xử lý như một giao dịch.

Một quy tắc đơn giản:

  • Chain khi các bước có ràng buộc khác nhau (upload chỉ Wi‑Fi, rồi một gọi API nhẹ).\n- Dùng một worker khi bạn cần một sync “tất cả hoặc không”.

WorkManager đảm bảo công việc được lưu, sống sót qua việc process chết và reboot, và tôn trọng constraints. Nó không đảm bảo thời gian chính xác, chạy ngay lập tức, hoặc chạy sau khi người dùng force-stop app. Nếu bạn xây app Android hiện trường (bao gồm app sinh bằng Kotlin từ AppMaster), thiết kế sync để việc chậm trễ là an toàn và được mong đợi.

Làm cho sync an toàn: idempotent, tăng dần, và có thể tiếp tục

Một app hiện trường sẽ chạy lại công việc. Điện thoại mất sóng, OS kill process, và người dùng bấm sync hai lần vì không thấy gì xảy ra. Nếu đồng bộ nền của bạn không an toàn khi chạy lại, bạn sẽ có bản ghi trùng lặp, cập nhật thiếu, hoặc vòng thử lại vô tận.

Bắt đầu bằng cách làm cho mỗi cuộc gọi server an toàn khi chạy hai lần. Cách đơn giản nhất là idempotency key cho mỗi mục (ví dụ UUID lưu cùng bản ghi cục bộ) mà server coi là "yêu cầu giống nhau, kết quả giống nhau". Nếu không thể thay server, dùng khóa tự nhiên ổn định và endpoint upsert, hoặc gửi số phiên bản để server từ chối cập nhật cũ.

Theo dõi trạng thái cục bộ một cách rõ ràng để worker có thể tiếp tục sau crash mà không đoán. Một máy trạng thái đơn giản thường đủ:

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

Giữ sync theo kiểu tăng dần. Thay vì "đồng bộ mọi thứ", lưu một con trỏ như lastSuccessfulTimestamp hoặc token do server phát. Đọc một trang nhỏ thay đổi, áp dụng chúng, rồi chỉ tiến con trỏ khi lô đó đã commit hoàn toàn cục bộ. Lô nhỏ (20-100 mục) giảm timeout, làm tiến độ rõ ràng, và giới hạn lượng công việc bạn phải lặp lại sau gián đoạn.

Làm uploads có thể tiếp tục nữa. Với ảnh hoặc payload lớn, lưu URI file và metadata upload, và chỉ đánh dấu uploaded sau khi server xác nhận. Nếu worker khởi động lại, nó tiếp tục từ trạng thái cuối cùng thay vì bắt đầu lại.

Ví dụ: một kỹ thuật viên điền 12 form và đính 8 ảnh dưới lòng đất. Khi thiết bị kết nối lại, worker upload theo lô, mỗi form có idempotency key, và con trỏ sync chỉ tiến sau khi mỗi lô thành công. Nếu app bị kill giữa chừng, chạy lại worker hoàn tất các mục còn lại trong hàng đợi mà không trùng lặp.

Ràng buộc phù hợp với điều kiện thiết bị thực tế

Constraints là hàng rào giữ cho sync nền không làm cạn pin, tiêu data, hoặc thất bại vào lúc tồi tệ nhất. Bạn muốn constraints phản ánh cách thiết bị hoạt động ở hiện trường, không phải trên bàn dev của bạn.

Bắt đầu với một tập nhỏ bảo vệ người dùng nhưng vẫn cho phép công việc chạy hầu hết ngày. Một baseline thực dụng: yêu cầu có mạng, tránh chạy khi pin thấp, và tránh khi lưu trữ gần đầy. Thêm "đang cắm sạc" chỉ nếu công việc nặng và không cần thời gian gấp, vì nhiều thiết bị hiện trường hiếm khi cắm sạc trong ca làm.

Quy định quá chặt là lý do phổ biến khiến "sync không bao giờ chạy". Nếu bạn yêu cầu Wi‑Fi không tính phí dữ liệu, đang cắm sạc, và pin không thấp, bạn cơ bản đang chờ khoảnh khắc hoàn hảo có thể không bao giờ đến. Nếu doanh nghiệp cần dữ liệu trong ngày, tốt hơn là chạy công việc nhỏ thường xuyên hơn chờ điều kiện lý tưởng.

Captive portal là một vấn đề thực tế khác: điện thoại báo đã kết nối nhưng người dùng phải bấm "Accept" trên trang Wi‑Fi khách sạn. WorkManager không thể phát hiện trạng thái đó đáng tin cậy. Hãy coi đó như thất bại bình thường: thử sync, timeout nhanh, và thử lại sau. Đồng thời giữ một thông báo trong app như "Đã kết nối Wi‑Fi nhưng không có internet" khi bạn phát hiện được trong request.

Dùng ràng buộc khác nhau cho upload nhỏ và lớn để app vẫn linh hoạt:

  • Payload nhỏ (cập nhật trạng thái, metadata form): bất kỳ mạng nào, pin không thấp.\n- Payload lớn (ảnh, video, gói bản đồ): ưu tiên mạng không tính phí nếu có thể, và cân nhắc yêu cầu cắm sạc.

Ví dụ: một kỹ thuật viên lưu form kèm 2 ảnh. Gửi trường form trên bất kỳ kết nối nào, nhưng đặt upload ảnh vào hàng đợi đợi Wi‑Fi hoặc khoảnh khắc tốt hơn. Văn phòng nhìn thấy công việc nhanh, và thiết bị không tiêu dữ liệu di động để tải ảnh nền.

Thử lại với backoff lũy thừa mà không làm người dùng bực mình

Triển khai nơi đội bạn vận hành
Triển khai lên AppMaster Cloud hoặc môi trường AWS, Azure, Google Cloud của bạn.
Triển khai App

Retries là nơi app hiện trường có thể khiến người dùng cảm thấy yên tâm hoặc thất vọng. Chọn chính sách backoff phù hợp với loại lỗi bạn mong đợi.

Backoff lũy thừa thường là mặc định an toàn cho mạng. Nó tăng thời gian chờ nhanh để bạn không dội server hoặc cạn pin khi vùng phủ kém. Backoff tuyến tính phù hợp với vấn đề tạm thời ngắn (ví dụ VPN chập chờn), nhưng thường retry quá nhiều ở vùng tín hiệu yếu.

Ra quyết định thử lại nên dựa trên loại lỗi, không chỉ "cái gì đó thất bại". Một bộ quy tắc đơn giản:

  • Timeout mạng, 5xx, DNS, không có kết nối: Result.retry()\n- Auth hết hạn (401): refresh token một lần, rồi fail và yêu cầu người dùng đăng nhập\n- Validation hoặc 4xx (bad request): Result.failure() kèm lỗi rõ cho support\n- Conflict (409) cho mục đã gửi: coi là thành công nếu sync của bạn idempotent

Giới hạn tổn hại để lỗi vĩnh viễn không lặp mãi. Đặt số lần thử tối đa, và sau đó dừng và hiển thị một thông báo im lặng, có thể hành động (không phải thông báo lặp lại).

Bạn cũng có thể thay đổi hành vi khi số lần thử tăng. Ví dụ, sau 2 lần thất bại, gửi lô nhỏ hơn hoặc bỏ qua upload lớn cho đến lần pull thành công tiếp theo.

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

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

Cách này giữ retries lịch sự: ít wakeups hơn, ít làm phiền người dùng, và phục hồi nhanh hơn khi kết nối trở lại.

Tiến trình hiển thị cho người dùng: notification, foreground work và trạng thái

Giữ quyền kiểm soát bằng mã nguồn
Nhận mã nguồn thực để đội bạn có thể xem xét, mở rộng và self-host khi cần.
Sinh mã

App hiện trường thường sync khi người dùng không ngờ: trong tầng hầm, trên mạng chậm, pin gần cạn. Nếu sync ảnh hưởng đến thứ người dùng đang chờ (uploads, gửi báo cáo, lô ảnh), hãy làm nó hiển thị và dễ hiểu. Công việc nền im lặng hợp lý cho cập nhật nhỏ, nhanh. Bất cứ việc nào lâu hơn nên minh bạch.

Khi cần chạy foreground

Dùng foreground execution khi job chạy lâu, cần thời gian, hoặc rõ ràng gắn với hành động người dùng. Trên Android hiện đại, upload lớn có thể bị dừng hoặc trì hoãn trừ khi bạn chạy foreground. Trong WorkManager, điều đó nghĩa là trả về một ForegroundInfo để hệ thống hiển thị notification ongoing.

Một notification tốt trả lời ba câu hỏi: đang đồng bộ gì, tiến đến đâu, và làm sao để dừng. Thêm hành động hủy rõ ràng để người dùng có thể rút lui nếu họ đang dùng dữ liệu có tính phí hoặc cần điện thoại ngay.

Tiến trình mà người dùng tin tưởng

Tiến trình nên phản ánh đơn vị thật, không phải phần trăm mơ hồ. Cập nhật tiến trình bằng setProgress và đọc từ WorkInfo trong UI (hoặc màn trạng thái).

Nếu bạn upload 12 ảnh và 3 form, báo "5 of 15 items uploaded", cho biết còn lại gì, và giữ lỗi gần nhất cho mục hỗ trợ.

Giữ tiến trình có ý nghĩa:

  • Số mục đã xong và còn lại\n- Bước hiện tại ("Uploading photos", "Sending forms", "Finalizing")\n- Thời gian sync thành công gần nhất\n- Lỗi gần nhất (ngắn, thân thiện với người dùng)\n- Tùy chọn hủy/dừng rõ ràng

Nếu đội bạn xây công cụ nội bộ nhanh với AppMaster, giữ cùng nguyên tắc: người dùng tin sync khi họ thấy nó, và khi nó phản ánh đúng điều họ muốn hoàn thành.

Unique work, tags, và tránh job đồng bộ trùng lặp

Job đồng bộ trùng lặp là một trong những cách dễ nhất làm cạn pin, tiêu data di động, và tạo xung đột server. WorkManager cung cấp hai công cụ đơn giản để ngăn điều đó: tên công việc duy nhất và tag.

Mặc định tốt là coi "sync" như một lane duy nhất. Thay vì enqueue job mới mỗi khi app wake, enqueue cùng một unique work name. Như vậy bạn không gặp cơn bão sync khi người dùng mở app, thay đổi mạng và job định kỳ cùng kích hoạt.

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

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

Chọn policy là quyết định hành vi chính:

  • KEEP: nếu đã có sync đang chạy (hoặc trong hàng đợi), bỏ qua yêu cầu mới. Dùng cho hầu hết nút "Sync now" và trigger tự động.\n- REPLACE: huỷ cái hiện tại và bắt đầu lại. Dùng khi inputs thực sự thay đổi, như người dùng đổi tài khoản hoặc chọn project khác.

Tags là tay cầm cho điều khiển và quan sát. Với tag ổn định như sync, bạn có thể huỷ, truy vấn trạng thái, hoặc lọc log mà không cần theo dõi ID cụ thể. Điều này hữu ích cho hành động "sync now" thủ công: bạn có thể kiểm tra xem đã có job chạy và hiện thông báo rõ ràng thay vì khởi thêm worker.

Periodic và on-demand sync không nên đấu với nhau. Giữ chúng tách biệt nhưng phối hợp:

  • Dùng enqueueUniquePeriodicWork("sync_periodic", KEEP, ...) cho job theo lịch.\n- Dùng enqueueUniqueWork("sync", KEEP, ...) cho on-demand.\n- Trong worker, thoát nhanh nếu không có gì để upload hoặc download, để lần chạy định kỳ rẻ.\n- Tùy chọn, để worker định kỳ enqueue cùng one-time unique sync, vậy mọi công việc thực sự xảy ra ở một nơi.

Những pattern này giữ sync nền dự đoán: một sync tại một thời điểm, dễ huỷ, và dễ quan sát.

Từng bước: pipeline đồng bộ nền thực tế

Xây dựng ứng dụng hiện trường nhanh hơn
Xây dựng ứng dụng sẵn sàng cho hiện trường với backend thật và client native, không cần viết tay toàn bộ.
Dùng thử AppMaster

Một pipeline sync đáng tin cậy dễ xây hơn khi bạn coi nó như một máy trạng thái nhỏ: mục công việc sống cục bộ trước, và WorkManager chỉ di chuyển chúng khi điều kiện phù hợp.

Một pipeline đơn giản bạn có thể phát hành

  1. Bắt đầu với bảng hàng đợi cục bộ. Lưu metadata nhỏ nhất bạn cần để tiếp tục: id item, type (form, photo, note), status (pending, uploading, done), attempt count, last error, và con trỏ hoặc revision server cho downloads.

  2. Với "Sync now" do người dùng bấm, enqueue một OneTimeWorkRequest với constraints phù hợp thực tế. Lựa chọn phổ biến là network connected và battery not low. Nếu upload nặng, yêu cầu đang cắm sạc.

  3. Triển khai một CoroutineWorker với các pha rõ ràng: upload, download, reconcile. Giữ mỗi pha tăng dần. Upload chỉ các mục pending, download chỉ thay đổi kể từ con trỏ, rồi reconcile xung đột với quy tắc đơn giản (ví dụ: server thắng cho trường assignment, client thắng cho note bản nháp).

  4. Thêm retries với backoff, nhưng chọn lọc về cái nên thử lại. Timeout và 500s nên thử lại. 401 nên fail nhanh và báo UI điều gì đã xảy ra.

  5. Quan sát WorkInfo để điều khiển UI và notification. Dùng progress update cho các pha như "Uploading 3 of 10", và hiển thị lỗi ngắn chỉ đường tiếp theo (thử lại, đăng nhập, kết nối 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()

Khi bạn giữ hàng đợi cục bộ và các pha worker rõ ràng, bạn có hành vi dự đoán được: công việc có thể tạm dừng, tiếp tục, và giải thích với người dùng mà không đoán điều gì đã xảy ra.

Sai lầm phổ biến và bẫy (và cách tránh)

Sync đáng tin cậy thường hỏng nhất vì vài lựa chọn nhỏ trông có vẻ vô hại khi test, nhưng vỡ trên thiết bị thực. Mục tiêu không phải chạy càng thường càng tốt. Mà là chạy đúng lúc, làm đúng việc, và dừng gọn khi không thể.

Những bẫy cần cảnh giác

  • Upload lớn mà không có constraints. Nếu bạn đẩy ảnh hoặc payload lớn trên mọi mạng và mọi mức pin, người dùng sẽ cảm nhận được. Thêm ràng buộc cho loại mạng và pin thấp, và chia công việc lớn thành mảng nhỏ.\n- Thử lại mọi lỗi mãi mãi. 401, token hết hạn, hoặc thiếu quyền không phải là vấn đề tạm thời. Đánh dấu là lỗi cứng, hiển thị hành động rõ ràng (đăng nhập lại), và chỉ thử lại các lỗi chuyển tiếp như timeout.\n- Vô tình tạo trùng lặp. Nếu worker có thể chạy hai lần, server sẽ thấy tạo hai bản nếu yêu cầu không idempotent. Dùng ID do client tạo ổn định cho mỗi mục và để server xử lý gửi lại như cập nhật, không phải tạo mới.\n- Dùng periodic work cho nhu cầu gần thời gian thực. Periodic work hợp cho bảo trì, không phải "sync now". Với sync do người dùng kích hoạt, enqueue one-time unique work và để người dùng trigger khi cần.\n- Báo "100%" quá sớm. Hoàn thành upload chưa chắc dữ liệu đã được server chấp nhận và reconcile. Theo dõi tiến trình theo giai đoạn (queued, uploading, server confirmed) và chỉ hiện xong sau khi có xác nhận.

Một ví dụ cụ thể: kỹ thuật viên nộp form với ba ảnh trong thang máy với sóng yếu. Nếu bạn bắt đầu ngay mà không có constraints, uploads dừng, retries tăng vọt, và form có thể bị tạo hai lần khi app khởi động lại. Nếu bạn ràng buộc tới mạng sử dụng được, upload theo bước, và gắn mỗi form với ID ổn định, cùng kịch bản kết thúc với một bản ghi server sạch và tiến trình trung thực.

Checklist nhanh trước khi phát hành

Kiểm thử logic đồng bộ không cần viết lại
Prototype hàng đợi upload, cơ chế thử lại và UX tiến trình với logic trực quan để tinh chỉnh sau.
Thử ngay

Trước khi release, test sync theo cách người dùng hiện trường thực sự phá hỏng nó: tín hiệu chập chờn, pin cạn, và nhiều lần bấm. Điều trông ổn trên điện thoại dev có thể vẫn vỡ trong thực tế nếu scheduling, retry, hoặc báo trạng thái sai.

Chạy các kiểm tra này trên ít nhất một thiết bị chậm và một thiết bị mới hơn. Giữ log, nhưng cũng quan sát những gì người dùng thấy trên UI.

  • Không mạng, rồi phục hồi: Bắt đầu sync khi tắt kết nối, sau đó bật lại. Xác nhận công việc được đưa vào hàng đợi (không fail nhanh), và tiếp tục sau mà không duplicate upload.\n- Khởi động lại thiết bị: Bắt đầu sync, reboot giữa chừng, rồi mở lại app. Xác minh công việc tiếp tục hoặc được re-schedule đúng, và app hiển thị trạng thái hiện tại đúng (không kẹt ở "syncing").\n- Pin yếu và lưu trữ thấp: Bật tiết kiệm pin, xuống dưới ngưỡng pin thấp nếu có thể, và làm đầy lưu trữ gần đầy. Xác nhận job chờ khi cần, rồi tiếp tục khi điều kiện cải thiện, không lặp retry làm cạn pin.\n- Kích hoạt lặp lại: Bấm nhiều lần nút "Sync" hoặc trigger sync từ nhiều màn hình. Cuối cùng bạn vẫn nên có một lần chạy sync logic, không phải đống workers song song tranh tài cùng record.\n- Lỗi server bạn có thể giải thích: Giả lập 500s, timeout và lỗi auth. Kiểm tra retry back off và dừng sau giới hạn, và người dùng thấy thông báo rõ ràng như "Không thể kết nối server, sẽ thử lại" thay vì lỗi chung chung.

Nếu bất kỳ test nào để app vào trạng thái không rõ ràng, coi đó là bug. Người dùng tha thứ cho sync chậm nhưng không tha thứ cho mất dữ liệu hoặc không biết chuyện gì xảy ra.

Ví dụ tình huống: forms offline và upload ảnh trong app hiện trường

Biến mô hình dữ liệu thành ứng dụng
Mô hình hóa dữ liệu trong vài phút và biến nó thành API và giao diện mà bạn có thể triển khai.
Bắt đầu xây dựng

Một kỹ thuật viên đến site có phủ sóng yếu. Họ điền form offline, chụp 12 ảnh, và bấm Submit trước khi đi. App lưu mọi thứ cục bộ trước (ví dụ trong database cục bộ): một bản ghi cho form, và một bản ghi cho mỗi ảnh với trạng thái rõ ràng như PENDING, UPLOADING, DONE, hoặc FAILED.

Khi họ bấm Submit, app enqueue một job sync duy nhất để không tạo trùng nếu họ bấm hai lần. Thiết lập phổ biến là chain WorkManager upload ảnh trước (nặng, chậm) rồi gửi payload form sau khi attachments được xác nhận.

Sync chỉ chạy khi điều kiện phù hợp thực tế. Ví dụ chờ có mạng kết nối, pin không thấp, và đủ lưu trữ. Nếu kỹ thuật viên vẫn ở tầng hầm không có sóng, không có gì tiêu pin lặp vô ích.

Tiến trình rõ ràng và thân thiện. Upload chạy foreground và hiển thị notification như “Uploading 3 of 12”, với nút Cancel rõ ràng. Nếu họ hủy, app dừng công việc và giữ các mục còn lại ở PENDING để retry sau mà không mất dữ liệu.

Retries lịch sự sau hotspot chập chờn: lần thất bại đầu thử lại sớm, nhưng mỗi lần thất bại tiếp theo đợi lâu hơn (backoff lũy thừa). Ban đầu cảm thấy phản hồi, rồi lùi để tránh cạn pin và spam mạng.

Với đội ops, lợi ích thực tế là: ít gửi trùng lặp hơn vì mục có idempotent và xếp hàng duy nhất, trạng thái thất bại rõ ràng (ảnh nào thất bại, vì sao và khi nào sẽ thử lại), và tin cậy hơn rằng “đã gửi” nghĩa là “đã lưu an toàn và sẽ sync”.

Bước tiếp theo: ưu tiên độ tin cậy trước, rồi mở rộng phạm vi sync

Trước khi thêm nhiều tính năng đồng bộ, làm rõ "xong" nghĩa là gì. Với hầu hết app hiện trường, nó không phải là "yêu cầu đã gửi". Mà là "server chấp nhận và xác nhận", cộng với trạng thái UI tương ứng. Một form ghi "Synced" nên giữ như vậy sau khi restart app, và form thất bại nên hiển thị cần làm gì tiếp theo.

Làm app dễ tin cậy bằng cách thêm vài tín hiệu nhỏ mà người dùng và support có thể nhìn thấy. Giữ chúng đơn giản và nhất quán across screens:

  • Thời gian sync thành công gần nhất\n- Lỗi sync gần nhất (thông điệp ngắn, không phải stack trace)\n- Mục đang chờ (ví dụ: 3 forms, 12 photos)\n- Trạng thái sync hiện tại (Idle, Syncing, Needs attention)

Đối với observability như một phần của tính năng. Nó tiết kiệm hàng giờ ở hiện trường khi ai đó ở vùng phủ sóng yếu không biết app có đang hoạt động hay không.

Nếu bạn xây cả backend và công cụ admin, làm cùng nhau giúp giữ hợp đồng sync ổn định. AppMaster (appmaster.io) có thể sinh backend production-ready, panel admin web, và native mobile apps, giúp giữ mô hình và auth đồng bộ trong khi bạn tập trung vào các cạnh khó của sync.

Cuối cùng, chạy một pilot nhỏ. Chọn một luồng end-to-end sync (ví dụ: "nộp form kiểm tra với 1-2 ảnh"), và phát hành nó với constraints, retries và tiến trình hiển thị cho người dùng hoạt động hoàn chỉnh. Khi lát cắt đó trở nên nhàm chán và dự đoán được, mở rộng từng tính năng một.

Câu hỏi thường gặp

What does “reliable background sync” actually mean in a field app?

Đồng bộ nền tin cậy nghĩa là công việc tạo trên thiết bị được lưu cục bộ trước và sẽ được tải lên sau mà không cần người dùng làm lại. Nó phải chịu được việc app bị kill, khởi động lại, mạng yếu và thử lại mà không mất dữ liệu hay tạo bản ghi trùng lặp.

When should I use OneTimeWorkRequest vs PeriodicWorkRequest for sync?

Dùng one-time work cho mọi thứ được kích hoạt bởi một sự kiện thực tế như “đã lưu form”, “thêm ảnh” hoặc người dùng bấm Sync. Dùng periodic work cho bảo trì và như mạng an toàn, nhưng không nên là con đường duy nhất cho upload quan trọng vì thời gian chạy có thể trôi.

Which Worker type should I choose: Worker, CoroutineWorker, or RxWorker?

Nếu dùng Kotlin và code sync của bạn dùng suspend functions, CoroutineWorker là lựa chọn đơn giản và dễ dự đoán, đặc biệt cho hủy tác vụ. Dùng Worker chỉ cho tác vụ chặn ngắn, và RxWorker chỉ khi app đã dựa nhiều vào RxJava.

Should I chain multiple workers or do everything in one worker?

Chain workers khi các bước có ràng buộc khác nhau hoặc cần thử lại riêng biệt (ví dụ: upload lớn chỉ trên Wi‑Fi rồi gọi API nhẹ trên bất kỳ mạng nào). Dùng một worker với các pha rõ ràng khi các bước chia sẻ trạng thái và bạn muốn hành vi “tất cả hoặc không” cho một lần sync logic.

How do I stop retries from creating duplicate records on the server?

Làm cho mỗi create/update an toàn khi chạy lại bằng cách dùng idempotency key cho từng mục (ví dụ UUID lưu cùng bản ghi cục bộ). Nếu không thể thay server, dùng upsert với khóa ổn định hoặc kiểm tra phiên bản để tránh tạo bản ghi mới khi gửi lại.

How do I make uploads resumable if the app is killed mid-sync?

Lưu trạng thái cục bộ rõ ràng như queued, uploading, uploaded, failed để worker có thể tiếp tục mà không phải đoán. Chỉ đánh dấu item là xong sau khi server xác nhận, và lưu metadata đủ (URI file, số lần thử) để hoàn tất sau crash hoặc reboot.

What constraints are a good default for field app sync jobs?

Bắt đầu với các ràng buộc tối thiểu bảo vệ người dùng nhưng vẫn cho phép sync chạy hàng ngày: yêu cầu kết nối mạng, tránh chạy khi pin yếu, và tránh khi dung lượng lưu trữ rất thấp. Cẩn thận với yêu cầu “unmetered” hoặc “đang cắm sạc” vì có thể khiến sync gần như không bao giờ chạy trên thiết bị hiện trường.

How should my app handle captive portals or “Wi‑Fi with no internet”?

Xử lý “kết nối nhưng không có internet” như một lỗi bình thường: timeout nhanh, trả Result.retry() và thử lại sau. Nếu bạn phát hiện được trong request, hiển thị thông báo đơn giản cho người dùng biết tại sao thiết bị có vẻ online nhưng sync không tiến triển.

What’s the safest retry strategy for spotty networks?

Dùng backoff lũy thừa cho lỗi mạng để thử lại thưa dần khi vùng phủ sóng kém. Thử lại cho timeout và lỗi 5xx, thất bại nhanh cho lỗi vĩnh viễn như request không hợp lệ, và giới hạn số lần thử để không lặp vô hạn khi người dùng phải can thiệp (ví dụ: đăng nhập lại).

How do I prevent “sync storms” and still show user-visible progress?

Enqueue sync như unique work để nhiều trigger không tạo các job song song, và hiển thị tiến trình người dùng có thể tin cậy cho các upload dài. Nếu công việc liên quan đến người dùng hoặc chạy lâu, chạy foreground với thông báo ongoing hiện số lượng thực và có nút hủy rõ ràng.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu