Kích hoạt khả năng ngoại tuyến trên Android

Ứng dụng Firebase 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ụ để lưu trữ dữ liệu cục bộ, quản lý sự hiện diện và xử lý độ trễ.

Độ bền của đĩa

Ứ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 trong bộ nhớ đệm khả dụng khi ngoại tuyến và Firebase gửi lại mọi nội dung 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ữ liên tục của ổ đĩa, ứng dụng của bạn sẽ ghi dữ liệu cục bộ vào thiết bị để ứng dụng của bạn có thể duy trì trạng thái khi 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.

Bạn có thể kích hoạt tính bền vững của ổ đĩa chỉ bằng một dòng mã.

Kotlin+KTX

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

Hành vi kiên trì

Bằng cách bật tính năng lưu giữ lâu dài, mọi dữ liệu mà ứng dụng khách Cơ sở dữ liệu thời gian thực Firebase sẽ đồng bộ hóa khi trực tuyến vẫn tồn tại trên đĩa và khả dụng 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ư trực tuyến bằng cách sử dụng dữ liệu cục bộ được lưu trữ trong bộ đệm. Lệnh gọi lại của trình nghe sẽ tiếp tục kích hoạt để cập nhật cục bộ.

Ứng dụng khách Cơ sở dữ liệu thời gian thực Firebase tự động giữ một hàng đợi tất cả các thao tác ghi được thực hiện khi ứng dụng của bạn ngoại tuyến. Khi tính năng lưu giữ được bật, hàng đợi này cũng được lưu vào đĩa để tất cả các thao tác ghi của bạn đều khả dụng 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 kết nối, tất cả các hoạt động sẽ được gửi đến máy chủ Cơ sở dữ liệu thời gian thực Firebase.

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

Giữ dữ liệu luôn mới

Cơ sở dữ liệu thời gian thực Firebase đồng bộ hóa và lưu trữ bản sao dữ liệu cục bộ cho người nghe đang hoạt động. Ngoài ra, bạn có thể giữ các vị trí cụ thể được đồng bộ hóa.

Kotlin+KTX

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

Java

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

Ứng dụng khách Cơ sở dữ liệu thời gian thực Firebase tự động tải xuống dữ liệu tại các vị trí này và giữ dữ liệu được đồng bộ hóa ngay cả khi tham chiếu không có trình xử lý đang hoạt động. Bạn có thể tắt đồng bộ hóa lại bằng dòng mã sau.

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

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

Truy vấn dữ liệu ngoại tuyến

Cơ sở dữ liệu thời gian thực Firebase lưu trữ dữ liệu được trả về từ truy vấn để sử dụng khi ngoại tuyến. Đối với các truy vấn được tạo khi ngoại tuyến, Cơ sở dữ liệu thời gian thực Firebase tiếp tục 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 được tải, Cơ sở dữ liệu thời gian thực Firebase sẽ tải dữ liệu từ bộ đệm cục bộ. Khi kết nối mạng khả dụng trở lại, dữ liệu sẽ tải và sẽ phản ánh truy vấn.

Ví dụ: mã này truy vấn bốn mục cuối cùng trong Cơ sở dữ liệu điểm số theo thời gian thực của Firebase

Kotlin+KTX

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ử rằng người dùng mất kết nối, ngoại tuyến và khởi động lại ứng dụng. Khi vẫn ngoại tuyến, ứng dụng sẽ truy vấn hai mục cuối cùng từ cùng một vị trí. Truy vấn này sẽ trả về thành công hai mục cuối vì ứng dụng đã tải tất cả bốn mục trong truy vấn trên.

Kotlin+KTX

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 khách Cơ sở dữ liệu thời gian thực Firebase tăng cường các sự kiện 'được thêm con' cho hai con khủng long có điểm cao nhất bằng cách sử dụng bộ nhớ đệm liên tục. Nhưng nó sẽ không đưa ra sự kiện 'giá trị' vì ứng dụng chưa bao giờ thực hiện truy vấn đó khi trực tuyến.

Nếu ứng dụng yêu cầu sáu mục cuối cùng khi ngoại tuyến, ứng dụng sẽ nhận được các sự kiện 'đã thêm con' cho bốn mục được lưu trong bộ nhớ đệm ngay lập tức. Khi thiết bị trực tuyến trở lại, ứng dụng khách Cơ sở dữ liệu thời gian thực Firebase sẽ đồng bộ hóa với máy chủ và nhận hai sự kiện 'được thêm con' và 'giá trị' cuối cùng 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 ngoại tuyến sẽ được xếp hàng đợi. Sau khi ứng dụng lấy lại được kết nối mạng, các giao dịch sẽ được gửi đến máy chủ Cơ sở dữ liệu thời gian thực.

Quản lý sự hiện diện

Trong các ứng dụng thời gian thực, việc phát hiện khi nào máy khách kết nối và ngắt kết nối thường rất hữu ích. 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.

Máy khách Cơ sở dữ liệu Firebase cung cấp các nguyên hàm đơn giản mà bạn có thể sử dụng để ghi vào cơ sở dữ liệu khi máy khách ngắt kết nối khỏi máy chủ Cơ sở dữ liệu Firebase. Các cập nhật này xảy ra cho dù máy khách có ngắt kết nối hoàn toàn hay không, vì vậy bạn có thể dựa vào chúng để dọn sạch dữ liệu ngay cả khi kết nối bị ngắt hoặc máy khách gặp sự cố. Tất cả các thao tác ghi, bao gồm cài đặt, cập nhật và xóa, có thể được thực hiện khi ngắt kết nối.

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

Kotlin+KTX

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 thức hoạt động của onDisconnect

Khi bạn thiết lập thao tác onDisconnect() , thao tác đó sẽ tồn tại trên máy chủ Cơ sở dữ liệu thời gian thực Firebase. Máy chủ kiểm tra 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 hoặc bị máy khách Cơ sở dữ liệu thời gian thực chủ động đóng, máy chủ sẽ kiểm tra 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+KTX

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());
        }
    }
});

Sự kiện onDisconnect cũng có thể bị hủy bằng cách gọi .cancel() :

Kotlin+KTX

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, việc ứng dụng của bạn biết khi nào nó trực tuyến hay ngoại tuyến sẽ rất hữu ích. Cơ sở dữ liệu thời gian thực Firebase cung cấp một vị trí đặc biệt tại /.info/connected được cập nhật mỗi khi trạng thái kết nối của máy khách Cơ sở dữ liệu thời gian thực Firebase thay đổi. Đây là một ví dụ:

Kotlin+KTX

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à giá trị boolean không được đồng bộ hóa giữa các máy khách Cơ sở dữ liệu thời gian thực vì giá trị này phụ thuộc vào trạng thái của máy khách. Nói cách khác, nếu một khách hàng đọc /.info/connected là sai, điều này không đảm bảo rằng một khách hàng riêng biệt cũng sẽ đọc sai.

Trên Android, Firebase tự động quản lý trạng thái kết nối để giảm băng thông và mức sử dụng pin. Khi khách hàng không có trình xử lý đang hoạt động, không có thao tác ghi hoặc onDisconnect đang chờ xử lý và không bị ngắt kết nối rõ ràng bằng phương thức goOffline , 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ủ

Máy chủ Cơ sở dữ liệu thời gian thực Firebase cung cấp 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 , cung cấp một cách dễ dàng để ghi chú một cách đáng tin cậy về thời điểm máy khách Cơ sở dữ liệu thời gian thực bị ngắt kết nối:

Kotlin+KTX

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);

Đồng hồ lệch

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

Kotlin+KTX

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 hiện diện mẫu

Bằng cách kết hợp các hoạt động ngắt kết nối với 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 hệ thống hiện diện 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í cơ sở dữ liệu để cho biết liệu máy khách Cơ sở dữ liệu thời gian thực có trực tuyến hay không. Khách hàng đặt vị trí này thành true khi họ trực tuyến và dấu thời gian khi họ ngắt kết nối. Dấu thời gian này cho biết lần cuối cùng người dùng cụ thể trực tuyến.

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

Đây là một hệ thống hiện diện người dùng đơn giản:

Kotlin+KTX

// 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");
    }
});