Как включить офлайн-режим

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

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

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

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

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

FirebaseDatabase.instance.setPersistenceEnabled(true);

Поведение при сохранении

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

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

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

Актуальность данных

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

final scoresRef = FirebaseDatabase.instance.ref("scores");
scoresRef.keepSynced(true);

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

scoresRef.keepSynced(false);

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

Запрос данных в офлайн-режиме

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

Например, следующий код запрашивает последние четыре элемента в базе данных оценок:

final scoresRef = FirebaseDatabase.instance.ref("scores");
scoresRef.orderByValue().limitToLast(4).onChildAdded.listen((event) {
  debugPrint("The ${event.snapshot.key} dinosaur's score is ${event.snapshot.value}.");
});

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

scoresRef.orderByValue().limitToLast(2).onChildAdded.listen((event) {
  debugPrint("The ${event.snapshot.key} dinosaur's score is ${event.snapshot.value}.");
});

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

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

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

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

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

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

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

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

Вот простой пример записи данных при отключении с помощью примитива onDisconnect:

final presenceRef = FirebaseDatabase.instance.ref("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().set("I disconnected!");

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

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

try {
    await presenceRef.onDisconnect().remove();
} catch (error) {
    debugPrint("Could not establish onDisconnect event: $error");
}

Событие onDisconnect также можно отменить, вызвав .cancel():

final onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.set("I disconnected");
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel();

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

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

final connectedRef = FirebaseDatabase.instance.ref(".info/connected");
connectedRef.onValue.listen((event) {
  final connected = event.snapshot.value as bool? ?? false;
  if (connected) {
    debugPrint("Connected.");
  } else {
    debugPrint("Not connected.");
  }
});

/.info/connected – это логическое значение, которое не синхронизируется между клиентами Realtime Database, поскольку зависит от состояния клиента. Другими словами, если один клиент считывает /.info/connected как false, это не гарантирует, что другой клиент также считает false.

Задержка обработки

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

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

final userLastOnlineRef =
    FirebaseDatabase.instance.ref("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().set(ServerValue.timestamp);

Расхождение часов

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

final offsetRef = FirebaseDatabase.instance.ref(".info/serverTimeOffset");
offsetRef.onValue.listen((event) {
  final offset = event.snapshot.value as num? ?? 0.0;
  final estimatedServerTimeMs =
      DateTime.now().millisecondsSinceEpoch + offset;
});

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

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

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

// 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 myConnectionsRef =
    FirebaseDatabase.instance.ref("users/joe/connections");

// Stores the timestamp of my last disconnect (the last time I was seen online)
final lastOnlineRef =
    FirebaseDatabase.instance.ref("/users/joe/lastOnline");

final connectedRef = FirebaseDatabase.instance.ref(".info/connected");
connectedRef.onValue.listen((event) {
  final connected = event.snapshot.value as bool? ?? false;
  if (connected) {
    final con = myConnectionsRef.push();

    // When this device disconnects, remove it.
    con.onDisconnect().remove();

    // When I disconnect, update the last time I was seen online.
    lastOnlineRef.onDisconnect().set(ServerValue.timestamp);

    // Add this device to my connections list.
    // This value could contain info about the device or a timestamp too.
    con.set(true);
  }
});