Back to Blog
FlutterOffline FirstDart

Xây dựng ứng dụng bền vững: hướng dẫn offline-first architecture trong Flutter

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?

Trong hệ sinh thái mobile ngày nay, người dùng kỳ vọng app phải phản hồi tức thì, bất kể trạng thái mạng của họ như thế nào. Đây chính là lúc offline-first architecture phát huy tác dụng. Thay vì coi mạng là điều hiển nhiên và chế độ offline chỉ là một trường hợp lỗi ngoại lệ, offline-first đảo ngược hoàn toàn logic đó. Thiết bị local trở thành source of truth chính, còn mạng chỉ là một tính năng bổ sung tùy chọn để sync dữ liệu. Theo hướng dẫn kiến trúc chính thức của Flutter, không có một cách duy nhất để triển khai điều này. Tùy thuộc vào yêu cầu của app, bạn có thể kết hợp linh hoạt các chiến lược đọc và ghi khác nhau. Hãy cùng tìm hiểu cách kiến trúc Flutter app của bạn để đạt được độ bền vững cao nhất.

1_7LavIQTMJYZbvXsoh1yJDA.webp

---

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.

  • Phù hợp nhất cho: Những app mà việc hiển thị dữ liệu mới nhất tuyệt đối là ưu tiên cao, nhưng hiển thị dữ liệu hơi cũ vẫn tốt hơn là màn hình lỗi.
  • 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à.

  • Phù hợp nhất cho: Social feed, dashboard, và các app ưu tiên thời gian tải tức thì nhưng vẫn cần hiển thị trạng thái mới nhất từ server.
  • 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.

  • Phù hợp nhất cho: Các app như theo dõi thời tiết hay đọc tin tức, nơi các lần sync diễn ra định kỳ ở background và người dùng không cần chờ phản hồi từ server để tiếp tục công việc.
  • ---

    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.

  • Ưu điểm: Đảm bảo local database không bao giờ lệch sync so với server.
  • Nhược điểm: Bạn mất khả năng ghi dữ liệu khi offline, phá vỡ cam kết cốt lõi của một trải nghiệm offline-first.
  • 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:

  • UI Feedback: Dù UI cập nhật tức thì, bạn có thể muốn hiển thị một chỉ báo nhỏ (như icon đám mây có gạch chéo) để cho người dùng biết một item đang chờ sync.
  • Conflict Resolution: Nếu người dùng chỉnh sửa một record khi offline, và người khác chỉnh sửa cùng record đó khi online, ai sẽ thắng? Bạn cần một chiến lược (như last-write-wins hoặc versioning timestamps).
  • App Lifecycles: Điều gì xảy ra nếu người dùng đóng app trước khi quá trình sync hoàn tất? Các công cụ như 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.

    Tài liệu tham khảo: Official Flutter Offline-First Guide

    Enjoyed this read?

    5likes

    Found this helpful?

    Support my work.

    If this post saved you time or taught you something, you can buy me a coffee. Every bit keeps me writing and shipping.

    Support my work.

    If this post saved you time or taught you something, you can buy me a coffee. Every bit keeps me writing and shipping.

    TP BankVietnam

    Scan with any VN banking app

    Show QRHide QR
    Scan to support via Vietnamese bank (VietQR)Scan to pay
    PayPalGlobal

    Pay with your PayPal account

    Support via PayPal
    Show QRHide QR
    Scan to support via PayPalScan to pay
    GitHubGlobal

    Visa / Mastercard · one-time or monthly

    Sponsor on GitHub
    Show QRHide QR
    Scan to sponsor on GitHubScan to pay
    Comments

    Loading…

    END OF SHEET — LET'S BUILD

    Got a hard problem? Let's talk.