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

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