Приложения 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"); } });