20 thg 8, 2025·8 phút đọc

Giải quyết xung đột biểu mẫu offline-first cho Kotlin + SQLite

Tìm hiểu cách giải quyết xung đột biểu mẫu offline-first: quy tắc merge rõ ràng, luồng sync Kotlin + SQLite đơn giản và các mẫu UX thực tế cho xung đột chỉnh sửa.

Giải quyết xung đột biểu mẫu offline-first cho Kotlin + SQLite

Điều gì thực sự xảy ra khi hai người chỉnh sửa offline

Biểu mẫu offline-first cho phép người dùng xem và sửa dữ liệu ngay cả khi mạng chậm hoặc không có kết nối. Thay vì chờ server, app ghi thay đổi vào cơ sở dữ liệu SQLite cục bộ trước, sau đó đồng bộ sau.

Điều đó khiến trải nghiệm cảm thấy tức thì, nhưng cũng tạo ra một thực tế đơn giản: hai thiết bị có thể thay đổi cùng một bản ghi mà không biết về nhau.

Một xung đột điển hình như sau: một kỹ thuật viên mở một work order trên tablet ở tầng hầm không có sóng. Họ đặt trạng thái là "Done" và thêm một ghi chú. Cùng lúc, một giám sát trên điện thoại khác cập nhật cùng work order, phân công lại và chỉnh sửa hạn chót. Cả hai nhấn Lưu. Cả hai lưu thành công ở máy cục bộ. Không ai làm gì sai.

Khi đồng bộ cuối cùng xảy ra, server phải quyết định bản ghi "thực" là gì. Nếu bạn không xử lý xung đột rõ ràng, thường sẽ gặp một trong các kết quả sau:

  • Last write wins: lần đồng bộ sau cùng ghi đè thay đổi trước và ai đó mất dữ liệu.
  • Lỗi cứng: đồng bộ bị từ chối và app hiện lỗi không hữu ích.
  • Bản ghi trùng lặp: hệ thống tạo bản sao thứ hai để tránh ghi đè và báo cáo trở nên rối.
  • Merge im lặng: hệ thống kết hợp thay đổi nhưng trộn các trường theo cách người dùng không mong đợi.

Xung đột không phải là lỗi. Chúng là kết quả dễ đoán của việc cho phép người dùng làm việc mà không có kết nối trực tiếp — điều mà offline-first hướng tới.

Mục tiêu là hai mặt: bảo vệ dữ liệu và giữ app dễ dùng. Thường điều đó có nghĩa là quy tắc merge rõ ràng (thường theo trường) và UX chỉ làm gián đoạn người dùng khi thực sự cần. Nếu hai sửa đổi chạm các trường khác nhau, bạn có thể thường hợp nhất im lặng. Nếu hai người thay đổi cùng một trường theo những cách khác nhau, app nên làm điều đó hiển thị và giúp ai đó chọn kết quả đúng.

Chọn chiến lược xung đột phù hợp với dữ liệu của bạn

Xung đột không phải là vấn đề kỹ thuật trước tiên. Chúng là quyết định sản phẩm về ý nghĩa của “đúng” khi hai người thay đổi cùng một bản ghi trước khi đồng bộ.

Ba chiến lược bao phủ hầu hết ứng dụng offline:

  • Last write wins (LWW): chấp nhận sửa đổi mới nhất và ghi đè sửa đổi cũ.
  • Review thủ công: dừng lại và hỏi người quản lý để chọn giữ gì.
  • Merge theo trường: kết hợp thay đổi từng trường và chỉ hỏi khi hai người đều chạm cùng một trường.

LWW có thể phù hợp khi tốc độ quan trọng hơn độ chính xác hoàn hảo và chi phí sai lầm thấp. Nghĩ đến ghi chú nội bộ, tag không quan trọng, hoặc trạng thái nháp có thể sửa lại sau.

Review thủ công là lựa chọn an toàn hơn cho các trường tác động lớn nơi app không nên đoán: văn bản pháp lý, xác nhận tuân thủ, số tiền payroll/invoice, thông tin ngân hàng, hướng dẫn dùng thuốc và mọi thứ có thể tạo ra trách nhiệm pháp lý.

Merge theo trường thường là mặc định tốt cho các biểu mẫu nơi vai trò khác nhau cập nhật những phần khác nhau. Một agent hỗ trợ sửa địa chỉ trong khi sales cập nhật ngày gia hạn. Merge theo trường giữ cả hai thay đổi mà không làm phiền ai. Nhưng nếu cả hai người đều sửa ngày gia hạn, trường đó nên kích hoạt quyết định.

Trước khi triển khai, viết ra ý nghĩa của “đúng” cho doanh nghiệp bạn. Một checklist nhanh giúp:

  • Trường nào phải luôn phản ánh giá trị thực mới nhất (ví dụ trạng thái hiện tại)?
  • Trường nào là lịch sử và không bao giờ bị ghi đè (ví dụ thời điểm đã gửi)?
  • Ai được phép thay đổi mỗi trường (vai trò, quyền sở hữu, phê duyệt)?
  • Nguồn chân lý khi giá trị mâu thuẫn là gì (thiết bị, server, phê duyệt quản lý)?
  • Nếu bạn chọn sai thì hậu quả ra sao (khó chịu nhỏ hay tác động tài chính/pháp lý)?

Khi những quy tắc này rõ ràng, mã sync chỉ còn một nhiệm vụ: thực thi chúng.

Định nghĩa quy tắc merge theo trường, không theo màn hình

Khi xung đột xảy ra, hiếm khi toàn bộ biểu mẫu bị ảnh hưởng đều nhau. Một người có thể cập nhật số điện thoại trong khi người khác thêm ghi chú. Nếu bạn coi toàn bộ bản ghi là tất cả hoặc không, bạn buộc người ta làm lại công việc tốt.

Merge theo trường dự đoán hơn vì mỗi trường có hành vi đã biết. UX giữ yên và nhanh.

Cách đơn giản để bắt đầu là chia trường thành "thường an toàn" và "thường không an toàn" để merge tự động.

Thường an toàn để merge tự động: ghi chú và comment nội bộ, tags, tệp đính kèm (thường là hợp nhất), và các timestamp như last contacted (thường giữ giá trị mới nhất).

Thường không an toàn để merge tự động: trạng thái/state, assignee/owner, tổng tiền/giá, cờ phê duyệt và số lượng tồn kho.

Sau đó chọn quy tắc ưu tiên cho mỗi trường. Các lựa chọn phổ biến: server wins, client wins, role wins (ví dụ manager ghi đè agent), hoặc tie-breaker xác định như phiên bản server mới nhất.

Câu hỏi then chốt là chuyện gì xảy ra khi cả hai phía thay đổi cùng một trường. Với mỗi trường, chọn một hành vi:

  • Tự động merge theo quy tắc rõ ràng (ví dụ tags là hợp nhất union)
  • Giữ cả hai giá trị (ví dụ thêm ghi chú với tên tác giả và thời gian)
  • Đánh dấu để review (ví dụ status và assignee cần người chọn)

Ví dụ: hai đại diện support cùng sửa ticket offline. Rep A đổi status từ "Open" sang "Pending". Rep B sửa notes và thêm tag "refund". Khi đồng bộ, bạn có thể an toàn merge notestags, nhưng không nên merge status im lặng. Chỉ hiện prompt cho status, còn mọi thứ khác đã được merge.

Để tránh tranh luận sau này, tài liệu hóa mỗi quy tắc bằng một câu cho mỗi trường:

  • "notes: giữ cả hai, thêm mới nhất vào cuối, ghi tên tác giả và thời gian."
  • "tags: hợp nhất union, chỉ xóa khi cả hai bên đều xóa rõ ràng."
  • "status: nếu thay đổi ở cả hai bên, yêu cầu người dùng chọn."
  • "assignee: manager wins, nếu không thì server wins."

Câu một dòng đó trở thành nguồn chân lý cho mã Kotlin, truy vấn SQLite, và UI xung đột.

Cơ bản mô hình dữ liệu: phiên bản và trường audit trong SQLite

Nếu bạn muốn xung đột dễ dự đoán, thêm một tập metadata nhỏ cho mỗi bảng được sync. Không có chúng, bạn không thể biết mình đang nhìn một chỉnh sửa mới, một bản sao cũ, hay hai chỉnh sửa cần merge.

Một mức tối thiểu thực tế cho mỗi bản ghi đồng bộ với server:

  • id (khóa chính ổn định): không tái sử dụng
  • version (integer): tăng sau mỗi write thành công trên server
  • updated_at (timestamp): khi bản ghi thay đổi lần cuối
  • updated_by (text hoặc id người dùng): ai là người chỉnh sửa cuối cùng

Trên thiết bị, thêm các trường chỉ dùng cục bộ để theo dõi thay đổi chưa được server xác nhận:

  • dirty (0/1): tồn tại thay đổi cục bộ
  • pending_sync (0/1): đang được xếp hàng để upload nhưng chưa xác nhận
  • last_synced_at (timestamp): lần cuối hàng này khớp server
  • sync_error (text, tuỳ chọn): lý do lỗi gần nhất để hiện trong UI

Optimistic concurrency là quy tắc đơn giản ngăn ghi đè im lặng: mỗi cập nhật kèm phiên bản bạn nghĩ mình đang sửa (một expected_version). Nếu record trên server vẫn ở phiên bản đó, update được chấp nhận và server trả về phiên bản mới. Nếu không, đó là xung đột.

Ví dụ: User A và User B đều download version = 7. A sync trước; server tăng lên 8. Khi B cố sync với expected_version = 7, server từ chối với xung đột để app của B merge thay vì ghi đè.

Để màn hình xung đột tốt, lưu lại điểm bắt đầu chung: cái mà người dùng ban đầu đã sửa từ đó. Hai cách thông dụng:

  • Lưu snapshot của bản ghi đã đồng bộ gần nhất (một cột JSON hoặc bảng song song).
  • Lưu change log (hàng cho mỗi edit hoặc mỗi trường edit).

Snapshot đơn giản hơn và thường đủ cho biểu mẫu. Change log nặng hơn nhưng có thể giải thích chính xác cái gì đã thay đổi, từng trường một.

Dù chọn cách nào, UI nên có khả năng hiển thị ba giá trị cho mỗi trường: sửa đổi của người dùng, giá trị hiện trên server, và điểm bắt đầu chung.

Snapshot bản ghi vs change log: chọn một cách

Xây ứng dụng offline-first
Xây luồng biểu mẫu offline-first với trạng thái đồng bộ rõ ràng và xử lý xung đột trong một dự án.
Dùng thử AppMaster

Khi đồng bộ biểu mẫu offline, bạn có thể upload bản ghi đầy đủ (snapshot) hoặc upload danh sách thao tác (change log). Cả hai đều hoạt động với Kotlin và SQLite, nhưng hành vi khác nhau khi hai người cùng chỉnh sửa một bản ghi.

Tuỳ chọn A: Snapshot toàn bộ bản ghi

Với snapshot, mỗi lần lưu ghi trạng thái đầy đủ mới nhất (tất cả trường). Khi đồng bộ, bạn gửi bản ghi kèm số phiên bản. Nếu server thấy phiên bản cũ, bạn gặp xung đột.

Điều này đơn giản để xây và nhanh khi đọc, nhưng thường tạo ra xung đột lớn hơn cần thiết. Nếu User A sửa số điện thoại trong khi User B sửa địa chỉ, snapshot có thể coi đó là một va chạm lớn dù các sửa đổi không chồng chéo.

Tuỳ chọn B: Change log (các thao tác)

Với change log, bạn lưu những gì đã thay đổi, không phải toàn bộ bản ghi. Mỗi chỉnh sửa cục bộ trở thành một operation có thể phát lại lên trạng thái server mới nhất.

Các operation dễ merge hơn:

  • Set một trường (set email thành giá trị mới)
  • Append một ghi chú (thêm một mục note mới)
  • Thêm một tag (thêm tag vào tập)
  • Xoá một tag (xoá một tag khỏi tập)
  • Đánh dấu checkbox hoàn thành (set isDone true kèm timestamp)

Operation logs có thể giảm xung đột vì nhiều hành động không chồng chéo. Thêm ghi chú hiếm khi xung đột với việc ai đó thêm ghi chú khác. Thêm/xóa tag có thể merge như toán tập. Với các trường đơn giá trị, bạn vẫn cần quy tắc cho từng trường khi hai chỉnh sửa khác nhau tranh nhau.

Đổi lại là phức tạp: cần ID operation ổn định, thứ tự (sequence cục bộ và thời gian server), và quy tắc cho các operation không giao hoán được.

Dọn dẹp: compact sau khi đồng bộ thành công

Operation logs lớn dần, nên lên kế hoạch thu gọn.

Cách phổ biến là compact theo bản ghi: khi tất cả operation tới một phiên bản server đã được xác nhận, gộp chúng thành snapshot mới, sau đó xoá các operation cũ. Giữ một đuôi ngắn chỉ khi bạn cần undo, audit, hoặc debug dễ hơn.

Luồng đồng bộ từng bước cho Kotlin + SQLite

Lấy mã nguồn thực
Xuất mã nguồn thực tế khi bạn cần toàn quyền kiểm soát Kotlin, SwiftUI, Go và Vue3.
Xây ngay

Chiến lược sync tốt phần lớn là về nghiêm túc với những gì bạn gửi và những gì bạn chấp nhận trả về. Mục tiêu là đơn giản: không bao giờ ghi đè dữ liệu mới hơn vô tình, và làm xung đột rõ ràng khi không thể merge an toàn.

Một luồng thực tế:

  1. Ghi mọi sửa đổi vào SQLite trước. Lưu thay đổi trong transaction cục bộ và đánh dấu bản ghi pending_sync = 1. Lưu local_updated_atserver_version đã biết gần nhất.

  2. Gửi một patch, không phải toàn bộ bản ghi. Khi có kết nối, gửi id bản ghi và chỉ các trường đã thay đổi, kèm expected_version.

  3. Cho server từ chối nếu version không khớp. Nếu version server hiện tại không khớp expected_version, server trả payload xung đột (bản ghi server, thay đổi đề xuất, và những trường khác nhau). Nếu khớp, server áp dụng patch, tăng version, và trả bản ghi đã cập nhật.

  4. Áp dụng auto-merge trước, sau đó hỏi người dùng. Chạy quy tắc merge theo trường. Xử lý các trường an toàn như notes khác với trường nhạy cảm như status, price, hoặc assignee.

  5. Commit kết quả cuối cùng và xóa flag pending. Dù đã auto-merge hay giải quyết thủ công, ghi kết quả cuối về SQLite, cập nhật server_version, đặt pending_sync = 0, và ghi đủ audit để sau này giải thích điều gì đã xảy ra.

Ví dụ: hai sales reps sửa cùng một đơn hàng offline. Rep A đổi ngày giao hàng. Rep B đổi số điện thoại khách hàng. Với patch, server có thể chấp nhận cả hai thay đổi một cách sạch sẽ. Nếu cả hai đổi ngày giao hàng, bạn hiện một quyết định rõ ràng thay vì buộc nhập lại toàn bộ.

Giữ lời hứa UI nhất quán: "Saved" nghĩa là đã lưu cục bộ. "Synced" là trạng thái riêng, rõ ràng.

Mẫu UX để giải quyết xung đột trong biểu mẫu

Xung đột nên là ngoại lệ, không phải luồng thường. Bắt đầu bằng cách auto-merge những gì an toàn, rồi chỉ hỏi người dùng khi thật sự cần quyết định.

Làm xung đột hiếm bằng mặc định an toàn

Nếu hai người sửa các trường khác nhau, merge mà không hiện modal. Giữ cả hai thay đổi và hiện một thông báo nhỏ "Đã cập nhật sau khi đồng bộ".

Dành lời nhắc cho va chạm thực sự: cùng một trường bị thay đổi ở cả hai thiết bị, hoặc một thay đổi phụ thuộc vào trường khác (như status kèm lý do status).

Khi phải hỏi, làm cho việc hoàn tất nhanh

Màn hình xung đột nên trả lời hai điều: gì đã thay đổi và cái gì sẽ được lưu. So sánh giá trị cạnh nhau: "Sửa của bạn", "Sửa của họ", và "Kết quả lưu". Nếu chỉ có hai trường xung đột, đừng hiện toàn bộ biểu mẫu. Nhảy thẳng đến các trường đó và giữ phần còn lại ở chế độ chỉ đọc.

Giữ các hành động giới hạn vào những thứ người dùng thực sự cần:

  • Giữ của tôi
  • Giữ của họ
  • Chỉnh sửa kết quả cuối
  • Xem từng trường (chỉ khi cần)

Merge từng phần là nơi UX phức tạp. Chỉ làm nổi bật các trường xung đột và gắn nhãn nguồn rõ ràng ("Của bạn" và "Của họ"). Chọn trước phương án an toàn để người dùng có thể xác nhận và tiếp tục.

Thiết lập kỳ vọng để người dùng không cảm thấy bị mắc kẹt. Nói rõ chuyện gì xảy ra nếu họ rời đi: ví dụ, "Chúng tôi sẽ giữ phiên bản của bạn cục bộ và thử đồng bộ lại sau" hoặc "Bản ghi này sẽ ở trạng thái Needs review cho đến khi bạn chọn." Hiện trạng đó rõ ràng trong danh sách để xung đột không bị lãng quên.

Nếu bạn xây flow này trong AppMaster, cùng cách tiếp cận UX vẫn áp dụng: auto-merge các trường an toàn trước, rồi hiển thị bước review tập trung chỉ khi các trường cụ thể va chạm.

Các trường hợp khó: xóa, trùng lặp và bản ghi “mất tích”

Xây full stack ở một nơi
Sinh backend, web và mobile production-ready từ một workspace no-code.
Dùng thử ngay

Hầu hết vấn đề sync cảm thấy ngẫu nhiên đến từ ba tình huống: ai đó xóa trong khi người khác chỉnh sửa, hai thiết bị tạo cùng một thứ offline, hoặc một bản ghi biến mất rồi xuất hiện lại. Những trường hợp này cần quy tắc rõ vì LWW thường khiến người dùng bất ngờ.

Xóa vs chỉnh sửa: ai thắng?

Quyết xem delete có mạnh hơn edit không. Trong nhiều ứng dụng doanh nghiệp, delete thắng vì người dùng mong một bản ghi bị xoá thì sẽ giữ nguyên bị xoá.

Một bộ quy tắc thực tế:

  • Nếu một bản ghi bị xóa trên bất kỳ thiết bị nào, coi nó là bị xóa ở mọi nơi, ngay cả khi có chỉnh sửa sau đó.
  • Nếu cần có thể hoàn tác deletes, chuyển "delete" thành trạng thái archived thay vì xóa cứng.
  • Nếu có chỉnh sửa đến một record đã bị xóa, giữ chỉnh sửa đó trong lịch sử để audit, nhưng đừng phục hồi bản ghi.

Va chạm tạo bản nháp trùng lặp offline

Biểu mẫu offline-first thường tạo ID tạm (như UUID) trước khi server cấp ID cuối. Trùng lặp xảy ra khi người dùng tạo hai bản nháp cho cùng một thực thể thực tế (cùng hoá đơn, cùng vé, cùng mặt hàng).

Nếu bạn có khóa tự nhiên ổn định (số hoá đơn, mã vạch, email + ngày), dùng nó để phát hiện va chạm. Nếu không, chấp nhận rằng trùng lặp sẽ xảy ra và cung cấp tuỳ chọn merge đơn giản sau đó.

Mẹo triển khai: lưu cả local_idserver_id trong SQLite. Khi server trả về, ghi mapping và giữ ít nhất cho tới khi chắc chắn không có thay đổi nào đang xếp hàng tham chiếu local ID.

Ngăn “phục sinh” sau khi đồng bộ

Phục sinh xảy ra khi Thiết bị A xóa một bản ghi, nhưng Thiết bị B offline sau đó upload một bản sao cũ như upsert, tạo lại bản ghi.

Cách khắc phục là tombstone. Thay vì xoá hàng ngay lập tức, đánh dấu nó bị xóa với deleted_at (thường kèm deleted_bydelete_version). Khi đồng bộ, xử lý tombstone như một thay đổi thực tế có thể ghi đè các trạng thái không bị xóa cũ hơn.

Quyết xem giữ tombstone trong bao lâu. Nếu người dùng có thể offline hàng tuần, giữ lâu hơn thời gian đó. Purge chỉ khi bạn chắc các thiết bị đã đồng bộ qua delete.

Nếu hỗ trợ undo, coi undo như một thay đổi khác: xoá deleted_at và tăng version.

Những lỗi phổ biến gây mất dữ liệu hoặc làm người dùng khó chịu

Ngăn ghi đè im lặng
Thiết lập cập nhật kiểu patch và kiểm tra version để dữ liệu mới không bị ghi đè im lặng.
Bắt đầu xây dựng

Nhiều thất bại sync xuất phát từ những giả định nhỏ nhưng âm thầm ghi đè dữ liệu tốt.

Lỗi 1: Tin vào thời gian thiết bị để sắp xếp sửa đổi

Điện thoại có thể sai giờ, múi giờ thay đổi, và người dùng có thể đặt giờ thủ công. Nếu bạn sắp xếp thay đổi theo timestamp thiết bị, cuối cùng bạn sẽ áp dụng chỉnh sửa sai thứ tự.

Ưu tiên version do server cấp (serverVersion) và coi timestamp client chỉ để hiển thị. Nếu phải dùng thời gian, thêm biện pháp an toàn và hoà giải trên server.

Lỗi 2: LWW vô tình trên các trường nhạy cảm

LWW có vẻ đơn giản cho đến khi gặp các trường không nên “thắng” bởi người đồng bộ sau cùng. Status, totals, approvals, assignments thường cần quy tắc rõ ràng hoặc review thủ công.

Checklist an toàn cho các trường rủi ro cao:

  • Xử lý chuyển trạng thái như một state machine, không phải edit tự do.
  • Tính lại tổng từ các line item. Đừng merge tổng như một số thô.
  • Với bộ đếm, merge bằng cách áp dụng delta, không chọn người thắng.
  • Với ownership/assignee, yêu cầu xác nhận rõ ràng khi xung đột.

Lỗi 3: Ghi đè giá trị server mới hơn bằng dữ liệu cache cũ

Điều này xảy ra khi client sửa một snapshot cũ, rồi upload toàn bộ bản ghi. Server chấp nhận và các thay đổi server mới hơn biến mất.

Sửa bằng cách thay đổi hình thức gửi: chỉ gửi trường thay đổi (hoặc change log), kèm phiên bản gốc bạn đã sửa. Nếu phiên bản gốc đã lỗi thời, server từ chối hoặc ép merge.

Lỗi 4: Không có lịch sử “ai thay đổi gì”

Khi xung đột xảy ra, người dùng muốn biết: tôi đã thay đổi gì và người kia đã thay gì? Nếu không có updatedBy và audit theo trường, màn hình xung đột trở thành đoán mò.

Lưu updatedBy, thời gian cập nhật do server nếu có, và ít nhất một audit trail nhẹ theo trường.

Lỗi 5: UI xung đột buộc so sánh toàn bộ bản ghi

Bắt người dùng so sánh toàn bộ bản ghi rất mệt. Phần lớn xung đột chỉ 1–3 trường. Hiển thị chỉ các trường xung đột, chọn sẵn phương án an toàn, và cho phép người dùng chấp nhận phần còn lại tự động.

Nếu bạn xây biểu mẫu trong công cụ no-code như AppMaster, mục tiêu vẫn là: giải quyết xung đột ở mức trường để người dùng chỉ cần một quyết định rõ ràng thay vì cuộn cả biểu mẫu.

Checklist nhanh và bước tiếp theo

Nếu bạn muốn các chỉnh sửa offline an toàn, coi xung đột là trạng thái bình thường chứ không phải lỗi. Kết quả tốt nhất đến từ quy tắc rõ ràng, test lặp lại, và UX giải thích những gì đã xảy ra bằng ngôn ngữ đơn giản.

Trước khi thêm tính năng, chắc chắn những điều cơ bản đã xong:

  • Với mỗi loại bản ghi, gán quy tắc merge cho từng trường (LWW, giữ max/min, append, union, hoặc luôn hỏi).
  • Lưu phiên bản do server kiểm soát cùng với updated_at do server, và xác thực chúng khi sync.
  • Chạy test hai thiết bị nơi cả hai đều chỉnh cùng một bản ghi offline, rồi đồng bộ theo cả hai thứ tự (A rồi B, B rồi A). Kết quả phải dự đoán được.
  • Test các xung đột khó: delete vs edit, và edit vs edit trên các trường khác nhau.
  • Hiện trạng rõ ràng: Synced, Pending upload, và Needs review.

Prototype toàn bộ luồng end-to-end với một biểu mẫu thực, không chỉ màn demo. Dùng kịch bản thực tế: một field tech cập nhật note công việc trên điện thoại trong khi dispatcher chỉnh title công việc trên tablet. Nếu họ chạm các trường khác nhau, auto-merge và hiện một gợi ý nhỏ "Updated from another device". Nếu họ chạm cùng một trường, điều hướng tới màn review đơn giản với hai lựa chọn và bản xem trước rõ ràng.

Khi sẵn sàng xây app mobile đầy đủ và API backend cùng nhau, AppMaster (appmaster.io) có thể giúp. Bạn có thể mô hình hóa dữ liệu, định nghĩa business logic, và xây UI web và native mobile trong một nơi, rồi triển khai hoặc xuất mã nguồn khi quy tắc sync đã ổn.

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

Xung đột đồng bộ offline là gì, giải thích ngắn gọn?

Xung đột xảy ra khi hai thiết bị thay đổi cùng một bản ghi được sao lưu trên server trong khi cả hai đều offline (hoặc trước khi bất kỳ thiết bị nào đồng bộ), và server sau đó thấy rằng cả hai cập nhật đều dựa trên cùng một phiên bản cũ. Hệ thống lúc đó phải quyết định giá trị cuối cùng cho mỗi trường khác nhau.

Nên chọn chiến lược xung đột nào: last write wins, review thủ công hay merge theo trường?

Bắt đầu với merge theo trường làm mặc định cho hầu hết biểu mẫu doanh nghiệp, vì các vai trò khác nhau thường chỉnh sửa các trường khác nhau và bạn có thể giữ cả hai thay đổi mà không làm phiền ai. Dùng review thủ công chỉ cho các trường có thể gây thiệt hại thực sự nếu đoán sai (tiền, phê duyệt, tuân thủ). Dùng last write wins chỉ cho các trường rủi ro thấp nơi việc mất sửa đổi cũ được chấp nhận.

Khi nào app nên hỏi người dùng để giải quyết xung đột?

Nếu hai sửa đổi chạm các trường khác nhau, thường bạn có thể tự động merge và giữ trải nghiệm yên tĩnh. Nếu hai sửa đổi thay đổi cùng một trường sang các giá trị khác nhau, trường đó nên kích hoạt quyết định, vì lựa chọn tự động có thể gây bất ngờ. Giữ phạm vi quyết định nhỏ bằng cách chỉ hiển thị các trường xung đột, không toàn bộ biểu mẫu.

Phiên bản bản ghi ngăn chặn ghi đè im lặng bằng cách nào?

Xử lý version như bộ đếm đơn điệu do server quản lý cho bản ghi, và yêu cầu client gửi expected_version với mỗi cập nhật. Nếu version trên server không khớp, server sẽ trả về lỗi xung đột thay vì ghi đè. Quy tắc đơn giản này ngăn chặn “mất dữ liệu im lặng” ngay cả khi hai thiết bị đồng bộ theo thứ tự khác nhau.

Bảng SQLite đồng bộ nên có metadata gì?

Một mức tối thiểu thực tế là id ổn định, version do server kiểm soát, và updated_at/updated_by do server cung cấp để bạn biết gì đã thay đổi. Trên thiết bị, theo dõi xem hàng có thay đổi và chờ upload (pending_sync) và giữ server_version đã đồng bộ lần cuối. Thiếu những trường này, bạn không thể phát hiện xung đột hay hiển thị màn hình giải quyết hữu ích.

Nên đồng bộ toàn bộ bản ghi hay chỉ các trường thay đổi?

Gửi chỉ những trường đã thay đổi (một patch) cùng expected_version. Upload toàn bộ bản ghi khiến các sửa đổi nhỏ, không chồng chéo thành xung đột không cần thiết và tăng nguy cơ ghi đè giá trị server mới hơn bằng dữ liệu cache cũ. Patch cũng làm rõ những trường cần quy tắc merge.

Nên lưu snapshot hay change log cho các chỉnh sửa offline?

Snapshot đơn giản hơn: lưu bản ghi đầy đủ gần nhất và so sánh khi cần. Change log linh hoạt hơn: lưu các thao tác như “set field” hay “append note” và phát lại lên trạng thái server mới nhất, thường merge tốt hơn cho notes, tags và các cập nhật cộng dồn khác. Chọn snapshot để triển khai nhanh; chọn change log nếu merge thường xuyên và bạn cần rõ ai đã thay đổi gì.

Nên xử lý xung đột xóa vs chỉnh sửa như thế nào?

Quyết trước xem delete có mạnh hơn edit hay không, vì người dùng mong đợi hành vi nhất quán. Mặc định an toàn là dùng tombstone (đánh dấu deleted_at và version) để một upsert offline cũ không vô tình phục hồi lại bản ghi. Nếu cần hoàn tác dễ dàng, dùng trạng thái “archived” thay vì xóa cứng.

Những lỗi phổ biến nào gây mất dữ liệu khi sync offline?

Đừng sắp xếp các ghi quan trọng theo thời gian thiết bị, vì đồng hồ sai lệch và múi giờ. Dùng version server cho thứ tự và kiểm tra xung đột. Tránh LWW trên các trường nhạy cảm như status, assignee, totals; cho những trường này quy tắc rõ ràng hoặc review thủ công. Và đừng buộc người dùng so sánh toàn bộ bản ghi khi chỉ một vài trường xung đột.

Làm sao để xây UX thân thiện với xung đột khi dùng AppMaster?

Giữ lời hứa rằng “Saved” nghĩa là đã lưu cục bộ và hiện một trạng thái riêng cho “Synced” để người dùng hiểu việc đang diễn ra. Nếu xây trong AppMaster, định nghĩa quy tắc merge theo trường trong product logic, tự động merge các trường an toàn, và chỉ đưa các xung đột trường thực sự vào bước review nhỏ. Kiểm thử với hai thiết bị chỉnh sửa cùng một bản ghi offline và đồng bộ theo cả hai thứ tự để kết quả dự đoán được.

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