Bật các chức năng ngoại tuyến trên Android

Các ứng dụng Firebase vẫn hoạt động ngay cả khi ứng dụng của bạn tạm thời mất kết nối mạng. Ngoài ra, Firebase còn cung cấp các công cụ để duy trì dữ liệu cục bộ, quản lý sự hiện diện và xử lý độ trễ.

Khả năng lưu trữ cố định dữ liệu trên đĩa

Các ứng dụng Firebase tự động xử lý tình trạng gián đoạn mạng tạm thời. Dữ liệu được lưu vào bộ nhớ đệm có sẵn khi không có mạng và Firebase sẽ gửi lại mọi lượt ghi khi kết nối mạng được khôi phục.

Khi bạn bật tính năng lưu trữ dữ liệu trên đĩa, ứng dụng sẽ ghi dữ liệu cục bộ vào thiết bị để ứng dụng có thể duy trì trạng thái khi không có mạng, ngay cả khi người dùng hoặc hệ điều hành khởi động lại ứng dụng.

Bạn có thể bật tính năng duy trì dữ liệu trên đĩa chỉ bằng một dòng mã.

Kotlin

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

Hành vi duy trì

Bằng cách bật tính năng duy trì, mọi dữ liệu mà ứng dụng Firebase Realtime Database sẽ đồng bộ hoá khi trực tuyến đều được duy trì trên đĩa và có sẵn ở chế độ ngoại tuyến, ngay cả khi người dùng hoặc hệ điều hành khởi động lại ứng dụng. Điều này có nghĩa là ứng dụng của bạn hoạt động như khi trực tuyến bằng cách sử dụng dữ liệu cục bộ được lưu trữ trong bộ nhớ đệm. Các lệnh gọi lại của trình nghe sẽ tiếp tục kích hoạt cho các bản cập nhật cục bộ.

Ứng dụng Firebase Realtime Database sẽ tự động duy trì một hàng đợi gồm tất cả các thao tác ghi được thực hiện khi ứng dụng của bạn không có kết nối mạng. Khi tính năng duy trì được bật, hàng đợi này cũng được duy trì trên đĩa để tất cả các thao tác ghi của bạn đều có sẵn khi người dùng hoặc hệ điều hành khởi động lại ứng dụng. Khi ứng dụng lấy lại được kết nối, tất cả các thao tác sẽ được gửi đến máy chủ Firebase Realtime Database.

Nếu ứng dụng của bạn sử dụng Xác thực Firebase, thì ứng dụng Firebase Realtime Database sẽ duy trì mã thông báo xác thực của người dùng trong các lần khởi động lại ứng dụng. Nếu mã thông báo xác thực hết hạn trong khi ứng dụng của bạn đang ở chế độ ngoại tuyến, thì ứng dụng sẽ tạm dừng các thao tác ghi cho đến khi ứng dụng xác thực lại người dùng. Nếu không, các thao tác ghi có thể không thành công do quy tắc bảo mật.

Luôn cập nhật dữ liệu

Firebase Realtime Database đồng bộ hoá và lưu trữ bản sao cục bộ của dữ liệu cho các trình nghe đang hoạt động. Ngoài ra, bạn có thể đồng bộ hoá các vị trí cụ thể.

Kotlin

val scoresRef = Firebase.database.getReference("scores")
scoresRef.keepSynced(true)

Java

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);

Ứng dụng Firebase Realtime Database sẽ tự động tải dữ liệu xuống tại các vị trí này và giữ cho dữ liệu luôn đồng bộ hoá ngay cả khi tham chiếu không có trình nghe đang hoạt động. Bạn có thể tắt lại tính năng đồng bộ hoá bằng dòng mã sau.

Kotlin

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

Theo mặc định, 10 MB dữ liệu đã đồng bộ hoá trước đó sẽ được lưu vào bộ nhớ đệm. Điều này là đủ cho hầu hết các ứng dụng. Nếu bộ nhớ đệm vượt quá kích thước đã định cấu hình, thì Firebase Realtime Database sẽ xoá dữ liệu được sử dụng gần đây nhất. Dữ liệu được đồng bộ hoá sẽ không bị xoá khỏi bộ nhớ đệm.

Truy vấn dữ liệu khi không có mạng

Firebase Realtime Database lưu trữ dữ liệu được trả về từ một truy vấn để sử dụng khi không có mạng. Đối với các truy vấn được tạo khi không có mạng, Firebase Realtime Database vẫn hoạt động đối với dữ liệu đã tải trước đó. Nếu dữ liệu được yêu cầu chưa tải, Firebase Realtime Database sẽ tải dữ liệu từ bộ nhớ đệm cục bộ. Khi có kết nối mạng trở lại, dữ liệu sẽ tải và phản ánh truy vấn.

Ví dụ: mã này truy vấn 4 mục cuối cùng trong một Firebase Realtime Database điểm số

Kotlin

val scoresRef = Firebase.database.getReference("scores")
scoresRef.orderByValue().limitToLast(4).addChildEventListener(object : ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) {
        Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}")
    }

    // ...
})

Java

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.orderByValue().limitToLast(4).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) {
        Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }

    // ...
});

Giả sử người dùng mất kết nối, chuyển sang chế độ ngoại tuyến và khởi động lại ứng dụng. Trong khi vẫn ở chế độ ngoại tuyến, ứng dụng sẽ truy vấn 2 mục gần đây nhất từ cùng một vị trí. Truy vấn này sẽ trả về thành công 2 mục cuối cùng vì ứng dụng đã tải cả 4 mục trong truy vấn ở trên.

Kotlin

scoresRef.orderByValue().limitToLast(2).addChildEventListener(object : ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) {
        Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}")
    }

    // ...
})

Java

scoresRef.orderByValue().limitToLast(2).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) {
        Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }

    // ...
});

Trong ví dụ trước, ứng dụng Firebase Realtime Database sẽ tạo sự kiện "đã thêm thành phần con" cho 2 khủng long có điểm số cao nhất bằng cách sử dụng bộ nhớ đệm được duy trì. Nhưng nó sẽ không tạo ra sự kiện "value" (giá trị), vì ứng dụng chưa bao giờ thực hiện truy vấn đó khi có mạng.

Nếu ứng dụng yêu cầu 6 mục gần đây nhất khi không có mạng, thì ứng dụng sẽ nhận được ngay các sự kiện "đã thêm trẻ" cho 4 mục được lưu vào bộ nhớ đệm. Khi thiết bị kết nối lại với mạng, ứng dụng Firebase Realtime Database sẽ đồng bộ hoá với máy chủ và nhận được 2 sự kiện cuối cùng là "child added" (đã thêm thành phần con) và "value" (giá trị) cho ứng dụng.

Xử lý giao dịch ngoại tuyến

Mọi giao dịch được thực hiện khi ứng dụng không có mạng đều được đưa vào hàng đợi. Sau khi ứng dụng có kết nối mạng trở lại, các giao dịch sẽ được gửi đến máy chủ Realtime Database.

Quản lý trạng thái hiện diện

Trong các ứng dụng theo thời gian thực, bạn nên phát hiện thời điểm máy khách kết nối và ngắt kết nối. Ví dụ: bạn có thể muốn đánh dấu người dùng là "ngoại tuyến" khi máy khách của họ ngắt kết nối.

Các ứng dụng cơ sở dữ liệu Firebase cung cấp các thành phần cơ bản đơn giản mà bạn có thể dùng để ghi vào cơ sở dữ liệu khi một ứng dụng ngắt kết nối với các máy chủ Cơ sở dữ liệu Firebase. Những bản cập nhật này xảy ra cho dù máy khách có ngắt kết nối một cách rõ ràng hay không, vì vậy, bạn có thể dựa vào những bản cập nhật này để dọn dẹp dữ liệu ngay cả khi kết nối bị ngắt hoặc máy khách gặp sự cố. Bạn có thể thực hiện tất cả các thao tác ghi, bao gồm cả thao tác thiết lập, cập nhật và xoá khi ngắt kết nối.

Sau đây là một ví dụ đơn giản về việc ghi dữ liệu khi ngắt kết nối bằng cách sử dụng nguyên tắc cơ bản onDisconnect:

Kotlin

val presenceRef = Firebase.database.getReference("disconnectmessage")
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!")

Java

DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!");

Cách hoạt động của onDisconnect

Khi bạn thiết lập một thao tác onDisconnect(), thao tác đó sẽ nằm trên máy chủ Firebase Realtime Database. Máy chủ kiểm tra tính bảo mật để đảm bảo người dùng có thể thực hiện sự kiện ghi được yêu cầu và thông báo cho ứng dụng của bạn nếu sự kiện đó không hợp lệ. Sau đó, máy chủ sẽ giám sát kết nối. Nếu tại bất kỳ thời điểm nào, kết nối hết thời gian chờ hoặc bị ứng dụng Realtime Database đóng chủ động, thì máy chủ sẽ kiểm tra tính bảo mật lần thứ hai (để đảm bảo thao tác vẫn hợp lệ) rồi gọi sự kiện.

Ứng dụng của bạn có thể sử dụng lệnh gọi lại trong thao tác ghi để đảm bảo onDisconnect được đính kèm chính xác:

Kotlin

presenceRef.onDisconnect().removeValue { error, reference ->
    error?.let {
        Log.d(TAG, "could not establish onDisconnect event: ${error.message}")
    }
}

Java

presenceRef.onDisconnect().removeValue(new DatabaseReference.CompletionListener() {
    @Override
    public void onComplete(DatabaseError error, @NonNull DatabaseReference reference) {
        if (error != null) {
            Log.d(TAG, "could not establish onDisconnect event:" + error.getMessage());
        }
    }
});

Bạn cũng có thể huỷ sự kiện onDisconnect bằng cách gọi .cancel():

Kotlin

val onDisconnectRef = presenceRef.onDisconnect()
onDisconnectRef.setValue("I disconnected")
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel()

Java

OnDisconnect onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.setValue("I disconnected");
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel();

Phát hiện trạng thái kết nối

Đối với nhiều tính năng liên quan đến sự hiện diện, ứng dụng của bạn nên biết thời điểm ứng dụng đang trực tuyến hoặc ngoại tuyến. Firebase Realtime Database cung cấp một vị trí đặc biệt tại /.info/connected. Vị trí này được cập nhật mỗi khi trạng thái kết nối của ứng dụng Firebase Realtime Database thay đổi. Dưới đây là một ví dụ:

Kotlin

val connectedRef = Firebase.database.getReference(".info/connected")
connectedRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val connected = snapshot.getValue(Boolean::class.java) ?: false
        if (connected) {
            Log.d(TAG, "connected")
        } else {
            Log.d(TAG, "not connected")
        }
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled")
    }
})

Java

DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        boolean connected = snapshot.getValue(Boolean.class);
        if (connected) {
            Log.d(TAG, "connected");
        } else {
            Log.d(TAG, "not connected");
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled");
    }
});

/.info/connected là một giá trị boolean không được đồng bộ hoá giữa các ứng dụng Realtime Database vì giá trị này phụ thuộc vào trạng thái của ứng dụng. Nói cách khác, nếu một ứng dụng đọc /.info/connected là false, thì điều này không đảm bảo rằng một ứng dụng riêng biệt cũng sẽ đọc false.

Trên Android, Firebase tự động quản lý trạng thái kết nối để giảm mức sử dụng băng thông và pin. Khi một ứng dụng không có trình nghe đang hoạt động, không có thao tác ghi hoặc onDisconnect đang chờ xử lý và không bị phương thức goOffline ngắt kết nối một cách rõ ràng, Firebase sẽ đóng kết nối sau 60 giây không hoạt động.

Xử lý độ trễ

Dấu thời gian của máy chủ

Các máy chủ Firebase Realtime Database cung cấp một cơ chế để chèn dấu thời gian được tạo trên máy chủ dưới dạng dữ liệu. Tính năng này, kết hợp với onDisconnect, giúp bạn dễ dàng ghi lại thời gian mà một ứng dụng Realtime Database đã ngắt kết nối một cách đáng tin cậy:

Kotlin

val userLastOnlineRef = Firebase.database.getReference("users/joe/lastOnline")
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)

Java

DatabaseReference userLastOnlineRef = FirebaseDatabase.getInstance().getReference("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);

Độ lệch đồng hồ

Mặc dù firebase.database.ServerValue.TIMESTAMP chính xác hơn nhiều và được ưu tiên cho hầu hết các thao tác đọc/ghi, nhưng đôi khi bạn có thể ước tính độ lệch đồng hồ của ứng dụng khách so với các máy chủ của Firebase Realtime Database. Bạn có thể đính kèm một lệnh gọi lại vào /.info/serverTimeOffset vị trí để lấy giá trị (tính bằng mili giây) mà các ứng dụng Firebase Realtime Database thêm vào thời gian được báo cáo cục bộ (thời gian bắt đầu tính bằng mili giây) để ước tính thời gian của máy chủ. Xin lưu ý rằng độ chính xác của độ lệch này có thể bị ảnh hưởng bởi độ trễ mạng. Do đó, độ lệch này chủ yếu hữu ích cho việc phát hiện những điểm khác biệt lớn (> 1 giây) về thời gian đồng hồ.

Kotlin

val offsetRef = Firebase.database.getReference(".info/serverTimeOffset")
offsetRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val offset = snapshot.getValue(Double::class.java) ?: 0.0
        val estimatedServerTimeMs = System.currentTimeMillis() + offset
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled")
    }
})

Java

DatabaseReference offsetRef = FirebaseDatabase.getInstance().getReference(".info/serverTimeOffset");
offsetRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        double offset = snapshot.getValue(Double.class);
        double estimatedServerTimeMs = System.currentTimeMillis() + offset;
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled");
    }
});

Ứng dụng mẫu Presence

Bằng cách kết hợp các thao tác ngắt kết nối với tính năng giám sát trạng thái kết nối và dấu thời gian của máy chủ, bạn có thể xây dựng một hệ thống trạng thái hoạt động của người dùng. Trong hệ thống này, mỗi người dùng lưu trữ dữ liệu tại một vị trí trong cơ sở dữ liệu để cho biết liệu ứng dụng Realtime Database có đang trực tuyến hay không. Các ứng dụng sẽ đặt vị trí này thành true khi chúng kết nối mạng và dấu thời gian khi chúng ngắt kết nối. Dấu thời gian này cho biết lần gần đây nhất mà người dùng được chỉ định truy cập trực tuyến.

Xin lưu ý rằng ứng dụng của bạn nên xếp hàng các thao tác ngắt kết nối trước khi người dùng được đánh dấu là đang trực tuyến, để tránh mọi điều kiện xung đột trong trường hợp kết nối mạng của máy khách bị mất trước khi cả hai lệnh có thể được gửi đến máy chủ.

Dưới đây là một hệ thống trạng thái hoạt động đơn giản của người dùng:

Kotlin

// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
val database = Firebase.database
val myConnectionsRef = database.getReference("users/joe/connections")

// Stores the timestamp of my last disconnect (the last time I was seen online)
val lastOnlineRef = database.getReference("/users/joe/lastOnline")

val connectedRef = database.getReference(".info/connected")
connectedRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val connected = snapshot.getValue<Boolean>() ?: false
        if (connected) {
            val con = myConnectionsRef.push()

            // When this device disconnects, remove it
            con.onDisconnect().removeValue()

            // When I disconnect, update the last time I was seen online
            lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)

            // Add this device to my connections list
            // this value could contain info about the device or a timestamp too
            con.setValue(java.lang.Boolean.TRUE)
        }
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled at .info/connected")
    }
})

Java

// Since I can connect from multiple devices, we store each connection instance separately
// any time that connectionsRef's value is null (i.e. has no children) I am offline
final FirebaseDatabase database = FirebaseDatabase.getInstance();
final DatabaseReference myConnectionsRef = database.getReference("users/joe/connections");

// Stores the timestamp of my last disconnect (the last time I was seen online)
final DatabaseReference lastOnlineRef = database.getReference("/users/joe/lastOnline");

final DatabaseReference connectedRef = database.getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        boolean connected = snapshot.getValue(Boolean.class);
        if (connected) {
            DatabaseReference con = myConnectionsRef.push();

            // When this device disconnects, remove it
            con.onDisconnect().removeValue();

            // When I disconnect, update the last time I was seen online
            lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);

            // Add this device to my connections list
            // this value could contain info about the device or a timestamp too
            con.setValue(Boolean.TRUE);
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled at .info/connected");
    }
});