Chắc ai cũng đã từng gặp cảnh này: mở app trên tàu điện, trong tòa nhà bê tông, hay ở vùng sâu vùng xa, và tất cả những gì bạn nhận được là một vòng tròn loading quay mãi không thôi. Bực bội lắm, phải không?

---
Khái Niệm Cốt Lõi: Repository Pattern
Nền tảng của offline-first architecture trong Flutter là Repository Pattern. Các Repository đóng vai trò là source of truth duy nhất cho UI của bạn.
Thay vì để UI gọi thẳng đến API, nó sẽ giao tiếp với một Repository. Repository hoạt động như một lớp trung gian, điều phối hai service riêng biệt:
1. ApiClientService: Xử lý các lời gọi HTTP REST đến backend.
2. DatabaseService: Quản lý việc lưu trữ dữ liệu local (dùng các công cụ như Isar, Drift, hoặc Hive).
Bằng cách tách biệt hai thứ này, UI chỉ đơn giản là yêu cầu dữ liệu từ Repository mà không cần biết dữ liệu đó đến từ local cache hay remote server.
---
Chiến Lược 1: Đọc Dữ Liệu
Khi đọc dữ liệu, mục tiêu của bạn là tốc độ và độ tin cậy. Tùy thuộc vào mức độ quan trọng của dữ liệu mới nhất đối với app, bạn có ba hướng tiếp cận chính:
Hướng A: Dùng Dữ Liệu Local Làm Fallback
Trong mô hình này, app sẽ cố gắng lấy dữ liệu mới nhất từ API trước. Nếu request thành công, nó cập nhật local database và trả về dữ liệu. Nếu lời gọi mạng thất bại (ví dụ thiết bị đang offline), app sẽ bắt lỗi và phục vụ dữ liệu đã được cache local.
Hướng B: Dùng Stream (Lợi Đủ Hai Đường)
Đây là tiêu chuẩn vàng cho các UI phản hồi nhanh. Repository trả về một Stream có thể emit nhiều giá trị. Đầu tiên, nó yield ngay dữ liệu đang được lưu local. Vì đọc từ local database cực kỳ nhanh, UI hiện ra tức thì. Trong lúc đó, một lời gọi mạng được thực hiện ở background. Khi dữ liệu mới từ remote về, database được cập nhật và Stream emit giá trị mới, cập nhật UI một cách mượt mà.
Hướng C: Chỉ Dùng Dữ Liệu Local
Trong hướng tiếp cận tách biệt hoàn toàn này, UI _chỉ_ đọc từ local database mà thôi. Một phương thức sync() riêng biệt được kích hoạt (thủ công qua pull-to-refresh, hoặc tự động theo background timer) để lấy dữ liệu remote và cập nhật local database.
---
Chiến Lược 2: Ghi Dữ Liệu
Việc ghi dữ liệu đặt ra một thách thức mới: điều gì xảy ra khi người dùng thực hiện thay đổi trong khi đang offline? Bạn có hai con đường chính ở đây:
Hướng A: Chỉ Ghi Khi Online
Trong hướng tiếp cận nghiêm ngặt này, app sẽ cố gắng đẩy dữ liệu lên API service trước. _Chỉ khi_ lời gọi API thành công, app mới lưu dữ liệu vào local database.
Hướng B: Ghi Theo Kiểu Offline-First
Đây mới là trải nghiệm offline-first thực sự. Khi người dùng tạo một record mới, ghi nó ngay lập tức vào local database và đánh dấu là "unsynced". UI phản hồi tức thì. Sau đó, một sync engine sẽ cố gắng đẩy thay đổi lên API.
class EntryRepository {
final DatabaseService _localDb;
final ApiClientService _remoteApi;
Future<void> createEntry(Entry entry) async {
// 1. Mark as unsynced
final newEntry = entry.copyWith(isSynced: false);
// 2. Save locally (UI updates instantly)
await _localDb.saveEntry(newEntry);
// 3. Attempt to sync to the remote server
_syncWithServer(newEntry);
}
Future<void> _syncWithServer(Entry entry) async {
try {
await _remoteApi.postEntry(entry);
// On success, update the local flag
await _localDb.updateEntry(entry.copyWith(isSynced: true));
} catch (e) {
// Network failed. Do nothing. The background sync will catch it later.
print('Network unavailable, will sync later.');
}
}
}
Nếu bạn chọn hướng này, bạn sẽ cần một Background Sync Engine. Dùng package như connectivity_plus, bạn có thể lắng nghe khi mạng được khôi phục và đẩy các item đang chờ:
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
if (result == ConnectivityResult.mobile || result == ConnectivityResult.wifi) {
_pushUnsyncedData(); // Fetches all isSynced == false and pushes them
}
});
---
Xử Lý Các Trường Hợp Đặc Biệt
Dù các khái niệm khá đơn giản, việc triển khai có thể khá phức tạp. Bạn sẽ cần cân nhắc:
workmanager có thể giúp bạn chạy các tác vụ background sync ngay cả khi Flutter engine đã bị tạm dừng.---
Kết Luận
Chuyển sang tư duy offline-first thay đổi cách bạn xây dựng app. Đây không phải một pattern cứng nhắc duy nhất, mà là một tập hợp các chiến lược — từ Stream cho đến background sync engine — mà bạn có thể tùy chỉnh theo nhu cầu cụ thể của app. Nó đòi hỏi nhiều boilerplate hơn ban đầu, nhưng thành quả mang lại rất xứng đáng. Kết quả là một app cảm giác cực kỳ nhanh, độ bền vững cao, và tôn trọng dữ liệu của người dùng bất kể họ đang ở đâu.
Loading…