Включение автономных возможностей на Android

Приложения Firebase работают даже в случае временной потери сетевого соединения. Кроме того, Firebase предоставляет инструменты для локального сохранения данных, управления присутствием и обработки задержек.

Сохранение данных на диске

Приложения Firebase автоматически обрабатывают временные перебои в сети. Кэшированные данные доступны в автономном режиме, и Firebase повторно отправляет все данные для записи при восстановлении сетевого соединения.

При включении сохранения данных на диске ваше приложение записывает данные локально на устройство, что позволяет ему сохранять состояние в автономном режиме, даже если пользователь или операционная система перезапустят приложение.

Включить сохранение данных на диск можно всего одной строкой кода.

Kotlin

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

Настойчивое поведение

Включение функции сохранения данных позволяет клиенту Firebase Realtime Database синхронизировать любые данные, которые он обычно синхронизирует в онлайн-режиме, на диск, делая их доступными в автономном режиме, даже после перезапуска приложения пользователем или операционной системой. Это означает, что ваше приложение будет работать так же, как и в онлайн-режиме, используя локальные данные, хранящиеся в кэше. Обратные вызовы слушателей будут продолжать срабатывать при локальных обновлениях.

Клиент Firebase Realtime Database автоматически поддерживает очередь всех операций записи, выполняемых, когда ваше приложение находится в автономном режиме. При включении функции сохранения данных эта очередь также сохраняется на диске, поэтому все ваши операции записи будут доступны, когда пользователь или операционная система перезапустят приложение. Когда приложение восстановит подключение к сети, все операции будут отправлены на сервер Firebase Realtime Database .

Если ваше приложение использует аутентификацию Firebase , клиент Firebase Realtime Database сохраняет токен аутентификации пользователя после перезапуска приложения. Если срок действия токена аутентификации истекает, пока ваше приложение находится в автономном режиме, клиент приостанавливает операции записи до тех пор, пока ваше приложение не проверит пользователя повторно; в противном случае операции записи могут завершиться неудачей из-за правил безопасности.

Поддержание актуальности данных

Firebase Realtime Database синхронизирует и хранит локальную копию данных для активных слушателей. Кроме того, вы можете синхронизировать данные в определенных местах.

Kotlin

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

Java

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

Клиент Firebase Realtime Database автоматически загружает данные в указанные места и поддерживает их синхронизацию, даже если у ссылки нет активных слушателей. Вы можете отключить синхронизацию с помощью следующей строки кода.

Kotlin

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

По умолчанию кэшируется 10 МБ ранее синхронизированных данных. Этого должно быть достаточно для большинства приложений. Если размер кэша превышает заданный размер, Firebase Realtime Database удаляет данные, которые использовались реже всего. Данные, которые синхронизируются, из кэша не удаляются.

Запрос данных в автономном режиме

Firebase Realtime Database хранит данные, полученные в результате запроса, для использования в автономном режиме. Для запросов, выполняемых в автономном режиме, Firebase Realtime Database продолжает работать с ранее загруженными данными. Если запрошенные данные не были загружены, Firebase Realtime Database загружает данные из локального кэша. Когда сетевое соединение снова становится доступным, данные загружаются и будут соответствовать запросу.

Например, этот код запрашивает последние четыре элемента из Firebase Realtime Database , содержащей оценки.

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

    // ...
});

Предположим, пользователь теряет соединение, переходит в офлайн-режим и перезапускает приложение. Находясь в офлайн-режиме, приложение запрашивает последние два элемента из того же местоположения. Этот запрос успешно вернет последние два элемента, поскольку приложение загрузило все четыре элемента из предыдущего запроса.

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

    // ...
});

В приведенном выше примере клиент Firebase Realtime Database генерирует события «дочерний элемент добавлен» для двух динозавров с наивысшим баллом, используя сохраненный кэш. Однако он не будет генерировать событие «значение», поскольку приложение никогда не выполняло этот запрос в режиме онлайн.

Если приложение запросит последние шесть элементов в автономном режиме, оно сразу же получит события «дочерние элементы добавлены» для четырех кэшированных элементов. Когда устройство снова подключится к сети, клиент Firebase Realtime Database синхронизируется с сервером и получит последние два события «дочерние элементы добавлены» и событие «значение» для приложения.

Обработка транзакций в автономном режиме

Все транзакции, выполняемые в автономном режиме приложения, ставятся в очередь. Как только приложение восстанавливает сетевое соединение, транзакции отправляются на сервер Realtime Database .

Управление присутствием

В приложениях, работающих в режиме реального времени, часто бывает полезно отслеживать подключение и отключение клиентов. Например, может потребоваться пометить пользователя как «оффлайн», когда его клиент отключается.

Клиенты Firebase Database предоставляют простые примитивы, которые можно использовать для записи в базу данных при отключении клиента от серверов Firebase Database. Эти обновления происходят независимо от того, происходит ли корректное отключение клиента или нет, поэтому вы можете рассчитывать на то, что они очистят данные даже в случае разрыва соединения или сбоя клиента. Все операции записи, включая установку, обновление и удаление, могут быть выполнены при отключении.

Вот простой пример записи данных при отключении с использованием примитива 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!");

Как работает onDisconnect

При создании операции onDisconnect() , она выполняется на сервере Firebase Realtime Database . Сервер проверяет безопасность, чтобы убедиться, что пользователь может выполнить запрошенное событие записи, и сообщает вашему приложению, если оно недействительно. Затем сервер отслеживает соединение. Если в какой-либо момент соединение истекает по таймауту или активно закрывается клиентом Realtime Database , сервер проверяет безопасность во второй раз (чтобы убедиться, что операция по-прежнему действительна), а затем вызывает событие.

Ваше приложение может использовать функцию обратного вызова при операции записи, чтобы убедиться, что onDisconnect было установлено корректно:

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

Событие onDisconnect также можно отменить, вызвав метод .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();

Определение состояния соединения

Для многих функций, связанных с определением присутствия, вашему приложению полезно знать, находится ли оно в сети или нет. Firebase Realtime Database предоставляет специальный путь /.info/connected , который обновляется каждый раз, когда изменяется состояние подключения клиента Firebase Realtime Database . Вот пример:

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 является логическим значением, которое не синхронизируется между клиентами Realtime Database , поскольку зависит от состояния клиента. Другими словами, если один клиент считывает /.info/connected как false, это не гарантирует, что другой клиент также считывает это значение как false.

На Android Firebase автоматически управляет состоянием соединения для снижения потребления полосы пропускания и заряда батареи. Если у клиента нет активных слушателей, ожидающих операций записи или onDisconnect , и он не отключен явно методом goOffline , Firebase закрывает соединение через 60 секунд бездействия.

Обработка задержки

Временные метки сервера

Серверы Firebase Realtime Database предоставляют механизм для вставки временных меток, сгенерированных на сервере, в качестве данных. Эта функция в сочетании с onDisconnect обеспечивает простой и надежный способ фиксации времени отключения клиента Realtime Database :

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

Смещение тактовой частоты

Хотя firebase.database.ServerValue.TIMESTAMP гораздо точнее и предпочтительнее для большинства операций чтения/записи, иногда может быть полезно оценить расхождение во времени клиента относительно серверов Firebase Realtime Database . Вы можете добавить функцию обратного вызова к адресу /.info/serverTimeOffset , чтобы получить значение в миллисекундах, которое клиенты Firebase Realtime Database добавляют к локальному сообщаемому времени (время эпохи в миллисекундах) для оценки времени сервера. Обратите внимание, что точность этого смещения может зависеть от задержки сети, поэтому оно полезно в основном для обнаружения больших (> 1 секунды) расхождений во времени.

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

Пример приложения "Присутствие"

Сочетая операции отключения с мониторингом состояния соединения и метками времени сервера, можно создать систему определения присутствия пользователя. В этой системе каждый пользователь сохраняет данные в определенном месте базы данных, чтобы указать, находится ли клиент Realtime Database в сети. Клиенты устанавливают это местоположение в значение true, когда подключаются к сети, и метку времени, когда отключаются. Эта метка времени указывает время последнего подключения данного пользователя к сети.

Обратите внимание, что ваше приложение должно ставить операции отключения в очередь до того, как пользователь будет отмечен как находящийся в сети, чтобы избежать состояний гонки в случае потери сетевого соединения клиента до того, как обе команды будут отправлены на сервер.

Вот простая система определения присутствия пользователя:

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