Người dùng mở danh sách công việc trong thang máy. Mạng không có, nhưng họ đã xem danh sách này trước đó. Nếu app chỉ hiện spinner rồi báo lỗi, sản phẩm đã bỏ phí dữ liệu còn nằm trên thiết bị.
Offline-first bắt đầu từ câu hỏi: người dùng vẫn làm được việc gì khi chưa liên lạc được với server? Bài này trình bày thiết kế áp dụng cho nhiều nền tảng, tham chiếu kiến trúc Android ngày 21/10/2026. Đây là hướng dẫn thiết kế, chưa phải một thư viện đồng bộ hoàn chỉnh.
1. Đừng gộp mọi tình huống thành “offline”
Ba trạng thái dưới đây cần UI và cách xử lý khác nhau:
| Trạng thái | Người dùng cần hiểu gì? |
|---|---|
| Có dữ liệu đã lưu, chưa tải được bản mới | Vẫn đọc được; dữ liệu có thể chưa cập nhật |
| Chưa từng có dữ liệu, hiện không tải được | Chưa thể hiển thị nội dung; cần thử lại |
| Đã thay đổi ở máy, server chưa xác nhận | Thao tác đang chờ đồng bộ hoặc cần xử lý lỗi |
Một dòng trạng thái “Cập nhật lần cuối lúc 14:05” thường có ý nghĩa hơn chỉ một biểu tượng mất mạng. Hãy hiển thị thời gian từ trạng thái dữ liệu thật, không cập nhật nhãn khi việc làm mới thất bại.
2. Chọn một đường đọc dữ liệu nhất quán
Hướng dẫn offline-first của Android đặt nguồn dữ liệu local làm nguồn đọc chuẩn cho các lớp phía trên. Kết quả tải từ mạng được đưa vào local, rồi UI nhận trạng thái mới từ đó.
Bạn có thể bắt đầu với sơ đồ:
UI <- Repository <- Local database
^
|
Đồng bộ dữ liệu
|
Network
Vai trò của repository là cung cấp giao diện dữ liệu phù hợp cho phần còn lại của app; chi tiết phối hợp nhiều nguồn nên được giữ ở lớp dữ liệu. Xem thêm tài liệu data layer.
Ví dụ, màn hình danh sách không cần tự quyết định lấy mảng nào giữa cache và response mới. Nó quan sát danh sách từ repository và nhận thêm trạng thái làm mới. Cách tổ chức này giúp bạn viết cùng một hành vi hiển thị cho cả lúc có và không có mạng.
3. Biểu diễn trạng thái thay vì suy đoán từ một boolean
Đây là kiểu dữ liệu minh họa bằng TypeScript, không phụ thuộc framework UI:
type SyncState =
| { kind: 'synced'; syncedAt: string }
| { kind: 'pending'; operationId: string }
| { kind: 'failed'; operationId: string; message: string };
type Task = {
id: string;
title: string;
completed: boolean;
sync: SyncState;
};
pending cần có dấu vết thao tác để khi người dùng mở lại app, bạn còn biết việc gì chưa được xác nhận. Một biến isOnline không cho biết request cuối cùng đã thành công hay thao tác đã được server ghi nhận chưa.
Với ví dụ công việc cá nhân, app có thể lưu thay đổi và bản ghi thao tác chờ trong cùng transaction local. Nếu app đóng ngay sau đó, lần mở tiếp theo vẫn biết cần đồng bộ gì. Đây là một lựa chọn thiết kế đề xuất; cách thực hiện transaction phụ thuộc database của dự án.
4. Quyết định cách ghi theo từng tính năng
Bạn không cần cho phép mọi hành động chạy offline ngay từ đầu. Hãy phân loại:
- Đọc dữ liệu đã có: hiển thị bản lưu và cho biết nó có thể đã cũ.
- Sửa ghi chú cá nhân: có thể lưu local rồi đồng bộ nếu yêu cầu sản phẩm chấp nhận.
- Hành động cần xác nhận tức thời từ hệ thống: mô tả rõ điều kiện cần mạng và giữ trạng thái chưa hoàn tất cho đến khi có kết quả.
Nếu cho phép gửi lại thao tác, client và server cần thống nhất cách nhận diện cùng một thao tác để tránh ghi lặp. Đừng coi một request timeout là bằng chứng rằng server chưa xử lý nó. Trước khi triển khai phần ghi offline, hãy thống nhất contract đồng bộ và trường hợp xung đột với phía backend.
5. Xử lý xung đột bằng quy tắc sản phẩm
Hai thiết bị có thể sửa cùng một công việc. Quy tắc “bản ghi đến sau thắng” dễ mô tả, nhưng có thể làm mất một thay đổi mà người dùng còn cần. Với dữ liệu quan trọng, bạn có thể cần giữ phiên bản, phát hiện xung đột hoặc cho người dùng hợp nhất nội dung.
Tài liệu Android nêu versioning và các chiến lược giải quyết xung đột. Với team nhỏ, nên chọn một trường hợp cụ thể để viết thành ví dụ: thiết bị A sửa tiêu đề, thiết bị B đánh dấu hoàn thành, cả hai đồng bộ theo thứ tự khác nhau. Kết quả mong muốn cần được quyết định trước khi viết worker retry.
Checklist kiểm tra
- Mở app lần đầu khi không có mạng: có hướng dẫn thử lại, không spinner vô hạn.
- Đã tải dữ liệu rồi tắt mạng: nội dung đã lưu vẫn đọc được.
- Tạo thao tác offline rồi đóng app: thao tác chờ còn tồn tại sau khi mở lại.
- Mạng chập chờn trong lúc đồng bộ: không nhân đôi thao tác hoặc báo thành công quá sớm.
- Hai thiết bị sửa cùng bản ghi: hành vi đúng quy tắc xung đột đã thống nhất.
Đây là các kịch bản kiểm thử đề xuất, không phải kết quả thử một ứng dụng cụ thể. Hãy bắt đầu với một màn hình đọc offline tốt, rồi mở rộng sang ghi và đồng bộ theo mức độ cần thiết của sản phẩm.





