Как включить офлайн-режим на 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");
    }
});

Пример приложения Presence

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