Как включить офлайн-режим на 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 Authentication, клиент 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 вызывает события child_added для двух динозавров с наивысшими баллами, используя сохраненный кеш. Но событие value не будет вызвано, поскольку приложение никогда не выполняло этот запрос в режиме онлайн.

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

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

Все транзакции, выполняемые в офлайн-режиме, ставятся в очередь. Когда сетевое подключение восстановится, транзакции будут отправлены на сервер 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");
    }
});