แนวทางการซิงค์พื้นหลังด้วย Kotlin WorkManager สำหรับแอปภาคสนาม
แนวทางการซิงค์พื้นหลังด้วย Kotlin WorkManager สำหรับแอปภาคสนาม: เลือกชนิดงานให้ถูกต้อง กำหนดข้อจำกัด ใช้ exponential backoff ในการลองใหม่ และแสดงความคืบหน้าให้ผู้ใช้เห็น

ความหมายของการซิงค์พื้นหลังที่เชื่อถือได้สำหรับแอปภาคสนามและแอปปฏิบัติการ
ในแอปภาคสนามและแอปปฏิบัติการ การซิงค์ไม่ใช่แค่ "สิ่งที่ดีที่จะมี" แต่มันคือวิธีที่งานหลุดออกจากอุปกรณ์และกลายเป็นเรื่องจริงสำหรับทีม เมื่อการซิงค์ล้มเหลว ผู้ใช้จะสังเกตเห็นเร็ว: งานที่เสร็จแล้วยังดูว่า "รอดำเนินการ" รูปหายไป หรืองานเดิมอัปโหลดซ้ำและสร้างข้อมูลซ้ำ
แอปแบบนี้ยากกว่าแอปผู้บริโภคทั่วไปเพราะโทรศัพท์ทำงานในสภาพที่เลวร้ายกว่า เครือข่ายสลับไปมาระหว่าง LTE, Wi‑Fi อ่อน, และไม่มีสัญญาณ ตัวประหยัดแบตเตอรี่บล็อกงานพื้นหลัง แอปถูกฆ่า ระบบปฏิบัติการอัปเดต และอุปกรณ์รีบูตระหว่างทาง การตั้งค่า WorkManager ที่เชื่อถือได้ต้องทนต่อเหตุการณ์เหล่านี้ได้โดยไม่สร้างปัญหา
การเชื่อถือได้โดยทั่วไปหมายถึงสี่ปัจจัย:
- สุดท้ายจะสอดคล้อง: ข้อมูลอาจมาช้าบ้าง แต่จะมาถึงโดยไม่ต้องคอยดูแลด้วยมือ
- ฟื้นตัวได้: ถ้าแอปตายกลางทางในการอัปโหลด การรันครั้งถัดไปจะดำเนินต่ออย่างปลอดภัย
- สังเกตได้: ผู้ใช้และฝ่ายสนับสนุนสามารถบอกได้ว่าสิ่งใดกำลังดำเนินหรือค้างอยู่
- ไม่ทำลาย: การลองใหม่จะไม่สร้างข้อมูลซ้ำหรือทำลายสถานะ
"Run now" เหมาะกับการกระทำขนาดเล็กที่ผู้ใช้เป็นคนกดและควรเสร็จเร็ว (เช่น ส่งสถานะเดียวก่อนผู้ใช้ปิดงาน) ส่วน "Wait" เหมาะกับงานหนัก เช่น การอัปโหลดรูปภาพ การอัปเดตเป็นชุด หรือสิ่งที่จะกินแบตหรือล้มเหลวง่ายในเครือข่ายไม่ดี
ตัวอย่าง: ผู้ตรวจบันทึกแบบฟอร์มพร้อมรูป 12 รูปในชั้นใต้ดินที่ไม่มีสัญญาณ การซิงค์ที่เชื่อถือได้จะเก็บทุกอย่างไว้ท้องถิ่น ทำเครื่องหมายว่าอยู่ในคิว และอัปโหลดทีหลังเมื่ออุปกรณ์มีการเชื่อมต่อจริง โดยไม่ให้ผู้ตรวจต้องทำงานซ้ำ
เลือกส่วนประกอบของ WorkManager ให้เหมาะสม
เริ่มจากการเลือกหน่วยงานของงานที่เล็กและชัดเจนที่สุด การตัดสินใจนี้มีผลต่อความเชื่อถือได้มากกว่าตรรกะการลองใหม่ที่ฉลาดใด ๆ ในภายหลัง
One-time vs periodic work
ใช้ OneTimeWorkRequest สำหรับงานที่ควรเกิดขึ้นเพราะมีการเปลี่ยนแปลง: แบบฟอร์มใหม่ถูกบันทึก รูปบีบเสร็จ หรือผู้ใช้กด Sync ให้คิวมันทันที (พร้อมข้อจำกัด) และปล่อยให้ WorkManager รันเมื่ออุปกรณ์พร้อม
ใช้ PeriodicWorkRequest สำหรับการบำรุงรักษาเป็นประจำ เช่น การดึงตรวจสอบอัปเดตหรือการทำความสะอาดรายคืน งานเชิงระยะไม่แม่นยำ มันมีช่วงเวลาขั้นต่ำและอาจเลื่อนได้ตามกฎแบตฯ และระบบ ดังนั้นอย่าใช้เป็นช่องทางเดียวสำหรับการอัปโหลดสำคัญ
รูปแบบที่ใช้งานได้จริงคือใช้งานแบบ one-time สำหรับ "ต้องซิงค์เร็ว" และใช้ periodic เป็นตาข่ายความปลอดภัย
เลือก Worker, CoroutineWorker, หรือ RxWorker
ถ้าคุณเขียน Kotlin และใช้ฟังก์ชัน suspend ให้เลือก CoroutineWorker มันทำให้โค้ดสั้นและการยกเลิกทำงานตามที่คาดไว้
Worker เหมาะกับโค้ดแบบบล็อกง่าย ๆ แต่ต้องระวังอย่าบล็อกนานเกินไป
RxWorker เหมาะเฉพาะเมื่อแอปใช้งาน RxJava อย่างหนัก มิฉะนั้นจะเพิ่มความซับซ้อนโดยไม่จำเป็น
เชนขั้นตอนหรือใช้ worker เดียวที่มีหลายเฟส?
การเชนดีเมื่อแต่ละขั้นตอนสามารถสำเร็จหรือล้มเหลวอย่างอิสระ และคุณต้องการการลองใหม่แยกกันและบันทึกที่ชัดเจน Worker ตัวเดียวที่มีหลายเฟสอาจดีกว่าเมื่อขั้นตอนแชร์ข้อมูลกันและต้องปฏิบัติเหมือนธุรกรรมเดียว
กฎง่าย ๆ:
- เชนเมื่อขั้นตอนมีข้อจำกัดต่างกัน (เช่น อัปโหลดเฉพาะ Wi‑Fi แล้วเรียก API เบา ๆ)
- ใช้ worker เดียวเมื่อคุณต้องการการซิงค์แบบ "ทั้งหมดหรือไม่มีเลย"
WorkManager รับประกันว่างานถูกเก็บถาวร สามารถทนต่อการตายของโปรเซสและรีบูตได้ และเคารพข้อจำกัด แต่มันไม่รับประกันเวลาแน่นอน การรันทันที หรือการรันหลังจากผู้ใช้บังคับปิดแอป หากคุณกำลังสร้างแอปภาคสนามบน Android ออกแบบการซิงค์ให้ยอมรับความล่าช้าได้
ทำให้การซิงค์ปลอดภัย: idempotent, incremental, resumable
แอปภาคสนามจะรันงานซ้ำ โทรศัพท์จะสูญเสียสัญญาณ ระบบปิดโปรเซส และผู้ใช้อาจกดซิงค์สองครั้งถ้าไม่เห็นผล ถ้าการซิงค์พื้นหลังของคุณไม่ปลอดภัยต่อการรันซ้ำ คุณจะได้ข้อมูลซ้ำ การอัปเดตหาย หรือการลองใหม่ไม่สิ้นสุด
เริ่มโดยทำให้การเรียกเซิร์ฟเวอร์ทุกครั้งปลอดภัยเมื่อรันซ้ำ วิธีที่ง่ายที่สุดคือใช้ idempotency key ต่อรายการ (เช่น UUID เก็บกับเรคอร์ดท้องถิ่น) ที่เซิร์ฟเวอร์ถือเป็น "คำขอเดียวกัน ผลลัพธ์เดียวกัน" ถ้าเปลี่ยนเซิร์ฟเวอร์ไม่ได้ ให้ใช้คีย์ธรรมชาติที่เสถียรและจุดสิ้นสุดแบบ upsert หรือรวมหมายเลขเวอร์ชันเพื่อให้เซิร์ฟเวอร์ปฏิเสธการอัปเดตเก่า
ติดตามสถานะท้องถิ่นอย่างชัดเจนเพื่อให้ worker สามารถต่อได้หลังแครชโดยไม่ต้องเดา เครื่องจักรสถานะง่าย ๆ มักพอเพียง:
- queued
- uploading
- uploaded
- needs-review
- failed-temporary
ทำให้การซิงค์เป็นแบบเพิ่มทีละน้อย แทนที่จะ "ซิงค์ทุกอย่าง" เก็บ cursor เช่น lastSuccessfulTimestamp หรือโทเค็นที่ออกจากเซิร์ฟเวอร์ ดึงหน้าข้อมูลเล็ก ๆ ประมวลผล แล้วเลื่อน cursor เมื่อชุดนั้นถูกยอมรับท้องถิ่นหมดแล้ว ชุดเล็ก (เช่น 20–100 รายการ) ลด timeout ทำให้เห็นความคืบหน้า และจำกัดงานที่ต้องทำซ้ำหลังการขัดจังหวะ
ทำให้อัปโหลดต่อจากจุดค้างได้ด้วย สำหรับรูปหรือ payload ขนาดใหญ่ ให้เก็บ URI ของไฟล์และเมตาดาต้าอัปโหลดไว้ และมาร์กเป็นอัปโหลดเมื่อเซิร์ฟเวอร์ยืนยันเท่านั้น ถ้า worker รีสตาร์ท มันจะต่อจากสถานะล่าสุดแทนที่จะเริ่มใหม่ทั้งหมด
ตัวอย่าง: ช่างเทคนิคกรอกฟอร์ม 12 รายการและแนบรูป 8 รูปใต้ดิน เมื่อต่อได้ worker จะอัปโหลดเป็นชุด แต่ละฟอร์มมี idempotency key และ cursor ก้าวหน้าเมื่อแต่ละชุดสำเร็จ หากแอปถูกฆ่ากลางทาง การรันซ้ำจะทำงานที่เหลือจนเสร็จโดยไม่ซ้ำข้อมูล
ข้อจำกัดที่สอดคล้องกับสภาพอุปกรณ์จริง
ข้อจำกัดเป็นราวกันตกที่ป้องกันการซิงค์พื้นหลังไม่ให้ดึงแบต หมดแพ็กเกจดาต้า หรือล้มเหลวในเวลาที่เลวร้ายที่สุด คุณต้องการข้อจำกัดที่สะท้อนการใช้งานจริงในภาคสนาม ไม่ใช่บนโต๊ะของคุณ
เริ่มด้วยชุดเล็ก ๆ ที่ปกป้องผู้ใช้แต่ยังให้งานรันได้บ่อย ๆ พื้นฐานที่ใช้งานได้คือ: ต้องมีเครือข่าย หลีกเลี่ยงการรันเมื่อแบตเตอรี่ต่ำ และหลีกเลี่ยงเมื่อพื้นที่เก็บข้อมูลวิกฤติ เพิ่ม "ชาร์จ" เฉพาะเมื่องานหนักและไม่รีบร้อน เพราะอุปกรณ์ภาคสนามหลายเครื่องไม่ค่อยเสียบชาร์จระหว่างกะ
การตั้งค่ามากเกินไปเป็นสาเหตุทั่วไปของปัญหา "การซิงค์ไม่เคยรัน" ถ้าคุณต้องการ Wi‑Fi ที่ไม่คิดเงิน ชาร์จ และแบตไม่ต่ำไปพร้อมกัน คุณกำลังรอช่วงเวลาที่สมบูรณ์แบบซึ่งอาจไม่เคยเกิด หากธุรกิจต้องการข้อมูลวันนี้ ดีกว่าที่จะรันงานเล็ก ๆ บ่อย ๆ แทนการรอเงื่อนไขที่สมบูรณ์
captive portals เป็นอีกปัญหาหนึ่ง: โทรศัพท์บอกว่าเชื่อมต่อ แต่ผู้ใช้ต้องกดยอมรับในหน้า Wi‑Fi สาธารณะ WorkManager ไม่สามารถตรวจจับสภาพนี้ได้เสมอ ถือเป็นความล้มเหลวปกติ: พยายามซิงค์ ตั้ง timeout ให้เร็ว แล้วลองใหม่ภายหลัง และให้ข้อความในแอปเช่น "เชื่อมต่อกับ Wi‑Fi แต่ไม่มีอินเทอร์เน็ต" เมื่อคุณตรวจพบได้ในคำขอ
ใช้ข้อจำกัดต่างกันสำหรับการอัปโหลดเล็กกับใหญ่เพื่อให้แอปยังตอบสนองได้:
- payload เล็ก (สถานะ, เมตาดาต้าฟอร์ม): เครือข่ายใดก็ได้, แบตไม่ต่ำ
- payload ใหญ่ (รูป, วิดีโอ, แพ็กแผนที่): เครือข่ายไม่คิดเงินเมื่อเป็นไปได้ และพิจารณา require charging
ตัวอย่าง: ช่างบันทึกฟอร์มกับรูป 2 รูป ส่งฟิลด์แบบฟอร์มบนการเชื่อมต่อใดก็ได้ แต่คิวการอัปโหลดรูปให้รอ Wi‑Fi หรือช่วงเวลาที่เหมาะ งานในสำนักงานจะเห็นงานเร็ว รูปจะไม่กินดาต้ามากในพื้นหลัง
การลองใหม่แบบ exponential backoff ที่ไม่รบกวนผู้ใช้
การลองใหม่เป็นจุดที่แอปภาคสนามจะรู้สึกสงบหรือพัง เลือกนโยบาย backoff ที่ตรงกับความผิดพลาดที่คาดไว้
exponential backoff มักเป็นค่าเริ่มต้นที่ปลอดภัยสำหรับเครือข่าย มันเพิ่มเวลารออย่างรวดเร็วเพื่อไม่ให้ถล่มเซิร์ฟเวอร์หรือใช้แบตมากเกินไปเมื่อสัญญาณไม่ดี linear backoff เหมาะกับปัญหาชั่วคราวสั้น ๆ แต่จะลองบ่อยเกินไปในพื้นที่สัญญาณอ่อน
ตัดสินใจลองใหม่ตามประเภทความล้มเหลว ไม่ใช่แค่ "มีบางอย่างล้มเหลว" กฎง่าย ๆ:
- เครือข่าย timeout, 5xx, DNS, ไม่มีการเชื่อมต่อ:
Result.retry() - หมดอายุการยืนยันตัวตน (401): รีเฟรชโทเค็นครั้งเดียว แล้วล้มเหลวและขอให้ผู้ใช้ลงชื่อเข้าใหม่
- Validation หรือ 4xx (bad request):
Result.failure()พร้อมข้อผิดพลาดที่ชัดเจนสำหรับฝ่ายสนับสนุน - Conflict (409) สำหรับรายการที่ส่งแล้ว: ถือว่าเป็นสำเร็จถ้าการซิงค์ของคุณเป็น idempotent
จำกัดความเสียหายเพื่อไม่ให้ข้อผิดพลาดถาวรวนซ้ำตลอดไป ตั้งจำนวนครั้งสูงสุด และหลังจากนั้นหยุดและแสดงข้อความที่เป็นประโยชน์หนึ่งครั้ง (ไม่ใช่แจ้งเตือนซ้ำ ๆ)
คุณยังสามารถเปลี่ยนพฤติกรรมเมื่อจำนวนครั้งเพิ่มขึ้น เช่น หลังความล้มเหลว 2 ครั้ง ส่งชุดเล็กลงหรือข้ามการอัปโหลดใหญ่จนกว่าจะมีการดึงข้อมูลสำเร็จครั้งถัดไป
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30, TimeUnit.SECONDS
)
.build()
// in doWork()
if (runAttemptCount >= 5) return Result.failure()
return Result.retry()
วิธีนี้ทำให้การลองใหม่สุภาพ: ลดการปลุกเครื่อง ลดการรบกวนผู้ใช้ และฟื้นตัวเร็วขึ้นเมื่อการเชื่อมต่อกลับมา
ความคืบหน้าที่ผู้ใช้เห็นได้: การแจ้งเตือน งานใน foreground และสถานะ
แอปภาคสนามมักซิงค์เมื่อผู้ใช้ไม่คาดคิด: ใต้ดิน ในเครือข่ายช้า แบตใกล้หมด ถ้าการซิงค์กระทบสิ่งที่ผู้ใช้รอ (การอัปโหลด รายงาน ชุดรูป) ให้แสดงและอธิบายได้ ช่วงพื้นหลังเงียบดีสำหรับอัปเดตเล็ก ๆ เร็ว ๆ แต่สิ่งที่ยาวควรมีความจริงใจ
เมื่อใดที่ต้องรันเป็น foreground work
ใช้การรัน foreground เมื่องานยาว จำเป็นตามเวลา หรือผูกชัดเจนกับการกระทำของผู้ใช้ บน Android สมัยใหม่ การอัปโหลดใหญ่จะถูกหยุดหรือเลื่อนเว้นแต่คุณรันเป็น foreground ใน WorkManager นั่นหมายถึงการคืนค่า ForegroundInfo เพื่อให้ระบบแสดงการแจ้งเตือนต่อเนื่อง
การแจ้งเตือนที่ดีตอบคำถามสามข้อ: กำลังซิงค์อะไร อยู่ถึงไหนแล้ว และจะหยุดอย่างไร ใส่ปุ่มยกเลิกชัดเจนเพื่อให้ผู้ใช้ยกเลิกได้หากอยู่บนดาต้ามีค่า หรือจำเป็นต้องใช้เครื่องทันที
ความคืบหน้าที่ผู้คนเชื่อถือได้
ความคืบหน้าควรสะท้อนหน่วยจริง ไม่ใช่เปอร์เซ็นต์คร่าว ๆ อัปเดตความคืบหน้าด้วย setProgress และอ่านจาก WorkInfo ใน UI (หรือหน้าสถานะ)
ถ้าคุณกำลังอัปโหลด 12 รูปและ 3 ฟอร์ม ให้รายงานว่า "อัปโหลด 5 จาก 15 รายการ" แสดงสิ่งที่เหลือ และเก็บข้อความผิดพลาดล่าสุดไว้สำหรับฝ่ายสนับสนุน
ให้ความคืบหน้ามีความหมาย:
- จำนวนรายการที่เสร็จและยังเหลือ
- ขั้นตอนปัจจุบัน ("กำลังอัปโหลดรูป", "กำลังส่งฟอร์ม", "กำลังสรุป")
- เวลาการซิงค์สำเร็จล่าสุด
- ข้อผิดพลาดล่าสุด (สั้น เข้าใจง่าย)
- ปุ่มยกเลิก/หยุดที่มองเห็นได้
ถ้าทีมของคุณสร้างเครื่องมือภายในอย่างรวดเร็วด้วย AppMaster ให้ยึดกฎเดียวกัน: ผู้ใช้เชื่อถือการซิงค์เมื่อเห็นมันและเมื่อมันตรงกับสิ่งที่พวกเขาพยายามทำ
งานที่เป็นเอกลักษณ์ แท็ก และการหลีกเลี่ยงงานซิงค์ซ้ำ
งานซิงค์ซ้ำเป็นหนึ่งในวิธีง่าย ๆ ที่ทำให้แบตหมด ใช้ดาต้ามาก และสร้างความขัดแย้งฝั่งเซิร์ฟเวอร์ WorkManager ให้เครื่องมือสองอย่างช่วยป้องกัน: ชื่องานแบบเอกลักษณ์และแท็ก
ค่าดีเริ่มต้นคือถือว่า "sync" เป็นเลนเดียว แทนที่จะคิวงานใหม่ทุกครั้งที่แอปตื่น ให้คิวงานชื่อเดียวกันแบบ unique work เช่นนี้จะไม่เกิดการพุ่งของ sync เมื่อผู้ใช้เปิดแอป การเปลี่ยนแปลงเครือข่ายเกิดขึ้น และงาน period ทำงานพร้อมกัน
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.addTag("sync")
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, request)
การเลือกนโยบายเป็นตัวกำหนดพฤติกรรมหลัก:
KEEP: ถ้ามีการซิงค์กำลังรัน (หรืออยู่ในคิว) ให้ละเว้นคำขอใหม่ ใช้สำหรับปุ่ม "Sync now" และทริกเกอร์ออโต้ซิงค์ส่วนใหญ่REPLACE: ยกเลิกอันที่กำลังรันแล้วเริ่มใหม่ ใช้เมื่ออินพุตเปลี่ยนจริง เช่น ผู้ใช้เปลี่ยนบัญชีหรือเลือกโปรเจกต์ต่างกัน
แท็กช่วยให้คุณควบคุมและมองเห็นได้ โดยใช้แท็กคงที่ เช่น sync คุณสามารถยกเลิก ตรวจสอบสถานะ หรือกรองบันทึกได้โดยไม่ต้องติดตาม ID เฉพาะ ซึ่งมีประโยชน์สำหรับการกระทำ "sync now" แบบแมนนวล: คุณสามารถเช็กว่ามีงานรันแล้วหรือไม่และแสดงข้อความแทนการเริ่ม worker ซ้ำ
การซิงค์แบบ periodic และ on-demand ไม่ควรต่อสู้กัน ให้แยกแต่ประสานกัน:
- ใช้
enqueueUniquePeriodicWork("sync_periodic", KEEP, ...)สำหรับงานที่ตั้งเวลาไว้ - ใช้
enqueueUniqueWork("sync", KEEP, ...)สำหรับ on-demand - ใน worker ของคุณ ให้ออกเร็วถ้าไม่มีอะไรจะอัปโหลดหรือดาวน์โหลด เพื่อให้การรันแบบ periodic ถูกเก็บเป็นเบา
- ทางเลือก: ให้ periodic worker คิว same one-time unique sync เพื่อให้งานจริงทั้งหมดเกิดที่เดียว
รูปแบบเหล่านี้ทำให้การซิงค์พื้นหลังคาดเดาได้: ซิงค์ทีละหนึ่ง ยกเลิกง่าย และสังเกตได้ง่าย
ขั้นตอนทีละขั้น: พายป์ไลน์การซิงค์พื้นหลังที่ใช้งานได้จริง
พายป์ไลน์การซิงค์ที่เชื่อถือได้สร้างง่ายขึ้นเมื่อคุณมองว่ามันคือเครื่องจักรสถานะเล็ก ๆ: งานอยู่ในเครื่องก่อน และ WorkManager ขยับมันไปข้างหน้าเมื่อเงื่อนไขพร้อม
พายป์ไลน์ง่าย ๆ ที่ส่งได้จริง
-
เริ่มด้วยตารางคิวท้องถิ่น เก็บเมตาดาต้าที่เล็กที่สุดที่จำเป็นเพื่อกู้สถานะ: id ของรายการ ประเภท (ฟอร์ม, รูป, โน้ต), สถานะ (pending, uploading, done), จำนวนครั้งที่ลอง, ข้อผิดพลาดล่าสุด และ cursor หรือ revision ของเซิร์ฟเวอร์สำหรับดาวน์โหลด
-
สำหรับ "Sync now" ที่ผู้ใช้กด ให้คิว
OneTimeWorkRequestพร้อมข้อจำกัดที่สอดคล้องกับโลกจริง ตัวเลือกทั่วไปคือเครือข่ายเชื่อมต่อและแบตไม่ต่ำ หากอัปโหลดหนัก ให้ต้องชาร์จด้วย -
ลง
CoroutineWorkerหนึ่งตัวที่มีเฟสชัดเจน: upload, download, reconcile เก็บแต่ละเฟสให้อยู่แบบเพิ่มทีละน้อย อัปโหลดเฉพาะรายการที่มาร์ก pending ดาวน์โหลดเฉพาะการเปลี่ยนแปลงตั้งแต่ cursor ล่าสุด แล้ว reconcile ความขัดแย้งด้วยกฎง่าย ๆ (เช่น: เซิร์ฟเวอร์ชนะสำหรับฟิลด์การมอบหมาย ลูกค้าเป็นผู้ชนะสำหรับร่างท้องถิ่น) -
เพิ่มการลองใหม่ด้วย backoff แต่เลือกอย่างระมัดระวังว่าจะลองใหม่เมื่อใด timeout และ 500 ควรถูกลองใหม่ 401 ควรล้มเร็วและบอก UI ว่าเกิดอะไรขึ้น
-
สังเกต
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 อย่างชัดเจน คุณจะได้พฤติกรรมที่คาดเดาได้: งานสามารถหยุด ชั่วคราว ต่อเนื่อง และอธิบายตัวเองให้ผู้ใช้โดยไม่ต้องเดาว่าเกิดอะไรขึ้น
ข้อผิดพลาดและกับดักที่พบบ่อย (และวิธีหลีกเลี่ยง)
การซิงค์ที่เชื่อถือได้ล้มเหลวบ่อยเพราะตัวเลือกเล็ก ๆ บางอย่างที่ดูไม่เป็นไรขณะทดสอบ แต่พังบนอุปกรณ์จริง เป้าหมายไม่ใช่ว่าจะรันบ่อยแค่ไหน แต่ว่าจะรันในเวลาที่ถูกต้อง ทำงานที่ถูกต้อง และหยุดอย่างสะอาดเมื่อไม่สามารถทำได้
กับดักที่ต้องระวัง
- อัปโหลดใหญ่โดยไม่มีข้อจำกัด ถ้าคุณส่งรูปหรือ payload ใหญ่บนเครือข่ายใดก็ได้และแบตใดก็ได้ ผู้ใช้จะรู้สึกได้ เพิ่มข้อจำกัดและแบ่งงานใหญ่เป็นชิ้นเล็ก ๆ
- ลองใหม่ทุกความล้มเหลวตลอดไป 401, โทเค็นหมดอายุ หรือสิทธิ์หายไปไม่ใช่ปัญหาชั่วคราว ทำให้เป็นความล้มเหลวถาวร แสดงการกระทำที่ชัดเจน (เช่น ลงชื่อเข้าใหม่) และลองเฉพาะปัญหาชั่วคราวจริง ๆ
- สร้างข้อมูลซ้ำโดยไม่ได้ตั้งใจ ถ้า worker รันสองครั้ง เซิร์ฟเวอร์จะเห็นการสร้างซ้ำเว้นแต่คำขอจะเป็น idempotent ใช้ ID ที่สร้างจากไคลเอนต์ต่อรายการและทำให้เซิร์ฟเวอร์จัดการคำขอซ้ำเป็นการอัปเดตไม่ใช่สร้างใหม่
- ใช้ periodic work สำหรับความต้องการแบบเกือบเรียลไทม์ Periodic work เหมาะสำหรับการบำรุงรักษา ไม่ใช่ "sync now" สำหรับการซิงค์ที่ผู้ใช้เริ่ม ให้คิว one-time unique work
- รายงาน "100%" เร็วเกินไป การอัปโหลดเสร็จไม่เท่ากับข้อมูลถูกยอมรับและ reconcile ติดตามความคืบหน้าตามขั้นตอน (queued, uploading, server confirmed) และแสดงว่าเสร็จจริงเมื่อได้รับการยืนยัน
ตัวอย่างที่เป็นรูปธรรม: ช่างส่งฟอร์มพร้อมรูปสามรูปในลิฟต์ที่สัญญาณอ่อน ถ้าคุณเริ่มทันทีโดยไม่มีข้อจำกัด อัปโหลดจะค้าง การลองใหม่จะพุ่ง และฟอร์มอาจถูกสร้างซ้ำเมื่อแอปรันใหม่ ถ้าคุณบังคับให้เครือข่ายที่ใช้งานได้ อัปโหลดเป็นขั้นตอน และกำหนดคีย์ฟอร์มที่เสถียร สถานการณ์เดียวกันจะจบด้วยเรคอร์ดเซิร์ฟเวอร์เดียวและข้อความความคืบหน้าที่ตรงไปตรงมา
เช็กลิสต์ด่วนก่อนปล่อย
ก่อนปล่อย ทดสอบการซิงค์แบบที่ผู้ใช้ภาคสนามจะทำให้มันพัง: สัญญาณขาดหาย แบตหมด และการกดหลายครั้ง สิ่งที่ดูดีบนเครื่อง dev อาจพังในภาคสนามถ้าการจัดตาราง การลองใหม่ หรือการรายงานสถานะผิดพลาด
รันการเช็กบนอุปกรณ์ช้าอย่างน้อยหนึ่งเครื่องและอุปกรณ์ใหม่หนึ่งเครื่อง เก็บบันทึก แต่ดูสิ่งที่ผู้ใช้เห็นใน UI ด้วย
- ไม่มีเครือข่ายแล้วกู้คืน: เริ่มซิงค์ขณะที่ไม่มีการเชื่อมต่อ แล้วเปิดการเชื่อมต่อ ยืนยันว่างานอยู่ในคิว (ไม่ล้มเหลวทันที) และกลับมาทำงานต่อโดยไม่ทำให้ข้อมูลซ้ำ
- รีบูตอุปกรณ์: เริ่มซิงค์ รีบูตกลางทาง แล้วเปิดแอปอีกครั้ง ตรวจสอบว่างานดำเนินต่อหรือถูกคิวใหม่อย่างถูกต้อง และแอปแสดงสถานะปัจจุบันถูกต้อง (ไม่ค้างที่ "syncing")
- แบตและพื้นที่เก็บข้อมูลต่ำ: เปิดโหมดประหยัดแบต ลดระดับแบต และเติมพื้นที่เก็บข้อมูลให้ใกล้เต็ม ยืนยันว่างานรอเมื่อควร แล้วดำเนินต่อเมื่อเงื่อนไขดีขึ้น โดยไม่วนในลูปการลองใหม่ใช้แบต
- ทริกเกอร์ซ้ำ: กดปุ่ม "Sync" หลายครั้ง หรือทริกเกอร์ซิงค์จากหลายหน้าจอ คุณควรได้การรันซิงค์ตรรกะเพียงหนึ่ง ไม่ใช่กอง worker ขนานที่แข่งกันทำงานเดียวกัน
- ความล้มเหลวของเซิร์ฟเวอร์ที่อธิบายได้: จำลอง 500s, timeout, และข้อผิดพลาดการยืนยันตัวตน เช็กว่าการลองใหม่ถอยออกและหยุดหลังขีดจำกัด และผู้ใช้เห็นข้อความชัดเจนเช่น "ไม่สามารถติดต่อเซิร์ฟเวอร์ จะลองใหม่" แทนข้อความล้มเหลวทั่วไป
ถ้าการทดสอบใดทิ้งแอปไว้ในสถานะไม่ชัดเจน ให้พิจารณาเป็นบั๊ก ผู้ใช้ยอมรับการซิงค์ช้า แต่ไม่ยอมให้ข้อมูลหายหรือไม่รู้ว่าเกิดอะไรขึ้น
ตัวอย่างสถานการณ์: ฟอร์มออฟไลน์และการอัปโหลดรูปในแอปภาคสนาม
ช่างมาถึงไซต์ที่สัญญาณอ่อน กรอกฟอร์มงานออฟไลน์ ถ่ายรูป 12 รูป แล้วกด Submit ก่อนออก แอปบันทึกทุกอย่างท้องถิ่นก่อน (เช่น ฐานข้อมูลท้องถิ่น): เรคอร์ดหนึ่งสำหรับฟอร์ม และหนึ่งเรคอร์ดต่อนรูป พร้อมสถานะชัดเจนเช่น PENDING, UPLOADING, DONE, หรือ FAILED
เมื่อกด Submit แอปคิวงานซิงค์แบบ unique เพื่อไม่ให้สร้างซ้ำถ้ากดสองครั้ง รูปแบบที่พบบ่อยคือ chain ของ WorkManager ที่อัปโหลดรูปก่อน (ใหญ่กว่า ช้ากว่า) แล้วส่ง payload ฟอร์มหลังจากแนบเรียบร้อย
การซิงค์จะรันก็ต่อเมื่อเงื่อนไขตรงกับโลกจริง เช่น เชื่อมต่ออยู่ แบตไม่ต่ำ และมีพื้นที่เพียงพอ ถ้าช่างยังอยู่ในชั้นใต้ดินที่ไม่มีสัญญาณ จะไม่มีการเผาแบตให้วนในพื้นหลัง
ความคืบหน้าชัดเจนและเข้าใจง่าย การอัปโหลดรันเป็น foreground work และแสดงการแจ้งเตือนเช่น "อัปโหลด 3 จาก 12" พร้อมปุ่ม Cancel ชัดเจน ถ้ายกเลิก แอปหยุดงานและเก็บรายการที่เหลือไว้ใน PENDING เพื่อให้ลองใหม่ภายหลังโดยไม่สูญหายข้อมูล
การลองใหม่สุภาพหลังจาก hotspot ผิดพลาด: ความล้มเหลวแรกลองใหม่เร็ว แต่แต่ละครั้งจะรอนานขึ้น (exponential backoff) รู้สึกตอบสนองตอนแรก แล้วค่อยถอยไปเพื่อไม่ให้ดึงแบตและสแปมเครือข่าย
สำหรับทีม ops ผลลัพธ์เป็นเชิงปฏิบัติ: ลดการส่งซ้ำเพราะรายการมี idempotency และคิวที่มีความชัดเจน สถานะความล้มเหลวชัดเจน (รูปไหนล้ม ทำไม และจะลองใหม่เมื่อไร) และความเชื่อมั่นว่า "submitted" หมายถึง "บันทึกอย่างปลอดภัยและจะซิงค์"
ขั้นตอนต่อไป: ให้ความสำคัญกับความน่าเชื่อถือก่อน แล้วขยายขอบเขตการซิงค์
ก่อนเพิ่มฟีเจอร์ซิงค์ ให้ชัดเจนว่าคำว่า "เสร็จ" หมายถึงอะไร สำหรับแอปภาคสนามส่วนใหญ่ มันไม่ใช่แค่ "คำขอถูกส่ง" แต่คือ "เซิร์ฟเวอร์ยอมรับและยืนยัน" พร้อมสถานะ UI ที่ตรงกับความเป็นจริง แบบฟอร์มที่บอกว่า "Synced" ควรอยู่เช่นนั้นหลังรีสตาร์ทแอป และแบบฟอร์มที่ล้มเหลวควรบอกว่าต้องทำอะไรต่อ
ทำให้แอปเชื่อถือได้ด้วยสัญญาณเล็ก ๆ ที่ผู้คนมองเห็นได้ (และฝ่ายสนับสนุนสามารถถามได้) เก็บให้เรียบง่ายและสอดคล้องข้ามหน้าจอ:
- เวลาซิงค์สำเร็จล่าสุด
- ข้อผิดพลาดซิงค์ล่าสุด (ข้อความสั้น ๆ ไม่ใช่สแตกเทรซ)
- รายการที่ค้าง (เช่น: 3 ฟอร์ม, 12 รูป)
- สถานะซิงค์ปัจจุบัน (Idle, Syncing, Needs attention)
ถือการสังเกตเป็นส่วนหนึ่งของฟีเจอร์ มันช่วยประหยัดเวลาในภาคสนามเมื่อคนอยู่ในสภาพเชื่อมต่ออ่อนและไม่รู้ว่าแอปกำลังทำงานหรือไม่
ถ้าคุณสร้าง backend และเครื่องมือแอดมินด้วยกัน การสร้างพร้อมกันจะช่วยรักษาสัญญาการซิงค์ให้คงที่ AppMaster (appmaster.io) สามารถสร้าง backend พร้อมใช้ แผงผู้ดูแลเว็บ และแอปมือถือเนทีฟ ซึ่งช่วยให้โมเดลและการยืนยันตัวตนสอดคล้องในขณะที่คุณโฟกัสกับมุมยากของการซิงค์
สุดท้าย ให้รันพิคอตเล็ก ๆ เลือกสไลซ์การซิงค์ครบจบหนึ่งอย่าง (เช่น "ส่งแบบฟอร์มตรวจสอบพร้อมรูป 1–2 ภาพ") และส่งมันพร้อมข้อจำกัด การลองใหม่ และความคืบหน้าที่ผู้ใช้เห็นได้ทำงานครบ เมื่อชิ้นนั้นน่าเบื่อและคาดเดาได้ ค่อยขยายฟีเจอร์ทีละอย่าง
คำถามที่พบบ่อย
การซิงค์พื้นหลังที่เชื่อถือได้หมายความว่างานที่สร้างบนอุปกรณ์จะถูกบันทึกไว้ในเครื่องก่อนและจะอัปโหลดเมื่อมีโอกาสโดยไม่ต้องให้ผู้ใช้ทำซ้ำขั้นตอน ควรทนต่อการปิดแอป รีบูต เครือข่ายอ่อน และการลองใหม่ โดยไม่สูญหายข้อมูลหรือสร้างรายการซ้ำ
ใช้ OneTimeWorkRequest สำหรับงานที่เกิดขึ้นเพราะมีการเปลี่ยนแปลงจริง เช่น บันทึกแบบฟอร์มใหม่ รูปภาพที่บีบเสร็จ หรือผู้ใช้กด Sync ใช้ PeriodicWorkRequest สำหรับงานรักษาระบบอย่างการตรวจสอบอัปเดตหรือการทำความสะอาดตอนกลางคืน แต่อย่าใช้เป็นช่องทางเดียวสำหรับการอัปโหลดสำคัญเพราะมันมีช่วงเวลาขั้นต่ำและอาจเลื่อนตามนโยบายระบบ
ถ้าคุณใช้ Kotlin และมีฟังก์ชัน suspend ให้เลือก CoroutineWorker เพราะโค้ดสั้นและการยกเลิกทำงานตามที่คาดไว้ Worker เหมาะกับงานบล็อกสั้น ๆ เท่านั้น และ RxWorker ใช้ได้เมื่อแอปของคุณใช้ RxJava อย่างหนักแล้วเท่านั้น
เชน worker เมื่อแต่ละขั้นตอนมีข้อจำกัดต่างกันหรือควรลองใหม่แยกกัน เช่น อัปโหลดบน Wi‑Fi แล้วค่อยเรียก API เล็ก ๆ ใช้งาน worker เดียวเมื่อขั้นตอนแชร์ข้อมูลกันและคุณต้องการพฤติกรรมแบบ "ทั้งหมดหรือไม่มีเลย" สำหรับการซิงค์เชิงตรรกะเดียว
ทำให้คำขอสร้าง/อัปเดตปลอดภัยเมื่อรันซ้ำ โดยใช้ idempotency key ต่อรายการ (เช่น UUID ที่เก็บกับเรคอร์ดท้องถิ่น) หากเปลี่ยนเซิร์ฟเวอร์ไม่ได้ ให้ใช้คีย์ธรรมชาติที่คงที่และจุดสิ้นสุดแบบ upsert หรือรวมหมายเลขเวอร์ชันเพื่อให้เซิร์ฟเวอร์ปฏิเสธข้อมูลเก่า
เก็บสถานะท้องถิ่นอย่างชัดเจน เช่น queued, uploading, uploaded, failed เพื่อให้ worker สามารถต่อจากจุดหยุดได้โดยไม่ต้องเดา อย่ามาร์กเป็นเสร็จจนกว่าเซิร์ฟเวอร์จะยืนยัน และเก็บเมตาดาต้าเช่น URI ของไฟล์และจำนวนครั้งที่ลองเพื่อให้สามารถเรียกคืนการอัปโหลดหลังแครชหรือรีบูตได้
เริ่มจากข้อจำกัดพื้นฐานที่ปกป้องผู้ใช้แต่ยังให้งานรันได้บ่อย ๆ: ต้องมีเครือข่าย หลีกเลี่ยงการรันเมื่อแบตเตอรี่ต่ำ และหลีกเลี่ยงเมื่อพื้นที่เก็บข้อมูลวิกฤติ เพิ่ม charging เฉพาะเมื่องานหนักและไม่เร่งด่วน เพราะอุปกรณ์ภาคสนามหลายเครื่องไม่ค่อยเสียบชาร์จระหว่างกะ
ถือว่า "เชื่อมต่อแต่ไม่มีอินเทอร์เน็ต" เป็นความล้มเหลวธรรมดา: ตั้งเวลาให้หมดเร็ว รีเทิร์น Result.retry() แล้วลองใหม่ภายหลัง ถ้าตรวจพบในคำขอได้ ให้แสดงข้อความสั้น ๆ ว่าต่อ Wi‑Fi แต่ไม่มีอินเทอร์เน็ต เพื่อให้ผู้ใช้เข้าใจว่าทำไมการซิงค์ไม่ก้าวหน้า
ใช้ exponential backoff สำหรับความล้มเหลวด้านเครือข่ายเพื่อให้การลองใหม่ช้าลงเมื่อสัญญาณไม่ดี รีเทิร์น Result.retry() สำหรับ timeout และข้อผิดพลาด 5xx ล้มเหลวทันทีสำหรับปัญหาถาวรเช่น 4xx ที่ไม่ถูกต้อง และจำกัดจำนวนครั้งสูงสุดเพื่อไม่ให้วนซ้ำตลอดไป
ใช้งานซิงค์แบบ unique work เพื่อหลีกเลี่ยงการรันพร้อมกัน แสดงความคืบหน้าที่เชื่อถือได้สำหรับการอัปโหลดยาว ๆ โดยถ้าจำเป็นให้รันเป็น foreground work พร้อมการแจ้งเตือนต่อเนื่องที่แสดงจำนวนรายการจริงและมีปุ่มยกเลิกชัดเจน


