Recursos off-line nas plataformas da Apple

Os apps do Firebase funcionam mesmo se o app perder temporariamente a conexão de rede. Além disso, o Firebase oferece ferramentas para armazenar dados no local de forma permanente, gerenciar a presença e lidar com a latência.

Persistência em disco

Os apps do Firebase administram automaticamente interrupções temporárias de rede. Os dados em cache estão disponíveis off-line, e o Firebase reenvia quaisquer gravações quando a conectividade de rede é restaurada.

Quando você ativa a persistência em disco, o app grava os dados localmente no dispositivo para que possa manter o estado enquanto estiver off-line, mesmo que o usuário ou o sistema operacional o reinicie.

É possível ativar a persistência em disco com apenas uma linha de código.

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
Database.database().isPersistenceEnabled = true

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[FIRDatabase database].persistenceEnabled = YES;

Comportamento da persistência

Ao ativar a persistência, quaisquer dados que o cliente do Firebase Realtime Database sincronizar on-line permanecerão no disco e estarão disponíveis off-line, mesmo quando o usuário ou o sistema operacional reiniciar o app. Isso significa que o app funciona como se estivesse on-line usando os dados locais armazenados no cache. As callbacks dos listeners continuam a ser acionados nas atualizações locais.

O cliente do Firebase Realtime Database mantém automaticamente uma fila de todas as operações de gravação executadas enquanto o app está off-line. Quando a persistência é ativada, essa fila também é mantida no disco para que todas as gravações estejam disponíveis quando o usuário ou o sistema operacional reiniciar o app. Quando o app recupera a conectividade, todas as operações são enviadas ao servidor do Firebase Realtime Database.

Se o app usa o Firebase Authentication, o cliente do Firebase Realtime Database mantém o token de autenticação do usuário entre as reinicializações do app. Se o token de autenticação expirar enquanto o app estiver off-line, o cliente pausará as operações de gravação até o app reautenticar o usuário. Caso contrário, as operações de gravação poderão falhar devido às regras de segurança.

Como manter os dados atualizados

O Firebase Realtime Database sincroniza e armazena uma cópia local dos dados para os listeners ativos. Além disso, você pode manter locais específicos em sincronia.

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.keepSynced(true)

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[scoresRef keepSynced:YES];

O cliente do Firebase Realtime Database faz o download automático dos dados nesses locais e os mantém sincronizados, mesmo que a referência não tenha listeners ativos. A sincronização pode ser desativada novamente com a linha de código a seguir.

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
scoresRef.keepSynced(false)

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[scoresRef keepSynced:NO];

Por padrão, 10 MB de dados previamente sincronizados são armazenados em cache. Isso é suficiente para a maioria dos apps. Se o cache ultrapassa o tamanho configurado, o Firebase Realtime Database limpa os dados que foram usados menos recentemente. Os dados mantidos em sincronia não são limpos no cache.

Como consultar dados off-line

O Firebase Realtime Database armazena os dados retornados de uma consulta para uso off-line. Nas consultas criadas off-line, o Firebase Realtime Database continua funcionando com os dados já carregados. Se os dados solicitados não foram carregados, o Firebase Realtime Database os carrega do cache local. Quando a conectividade de rede estiver novamente disponível, os dados serão carregados e refletirão a consulta.

Por exemplo, este código consulta os últimos quatro itens em um Firebase Realtime Database de pontuações:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
let scoresRef = Database.database().reference(withPath: "scores")
scoresRef.queryOrderedByValue().queryLimited(toLast: 4).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"];
[[[scoresRef queryOrderedByValue] queryLimitedToLast:4]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

Suponha que o usuário perca a conexão, fique off-line e reinicie o app. Enquanto está off-line, o app consulta os dois últimos itens no mesmo local. Essa consulta retorna com êxito os dois últimos itens porque o app carregou todos os quatro itens na consulta acima.

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
scoresRef.queryOrderedByValue().queryLimited(toLast: 2).observe(.childAdded) { snapshot in
  print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")")
}

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[[[scoresRef queryOrderedByValue] queryLimitedToLast:2]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
      NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value);
    }];

No exemplo anterior, o cliente do Firebase Realtime Database gera eventos child added para os dois dinossauros com pontuação mais alta usando o cache mantido. Porém, isso não gera um evento value (valor), já que o app nunca executou essa consulta enquanto estava on-line.

Se o app tivesse que solicitar os últimos seis itens enquanto estivesse off-line, ele receberia eventos child added para os quatro itens em cache imediatamente. Quando o dispositivo fica on-line novamente, o cliente do Firebase Realtime Database é sincronizado com o servidor e recebe os dois eventos child added finais e os eventos value para o app.

Como gerenciar transações off-line

Todas as transações que são executadas enquanto o app está off-line são colocadas em fila. Assim que o app recupera a conectividade de rede, as transações são enviadas ao servidor do Realtime Database.

Como gerenciar presença

Nos apps em tempo real, costuma ser útil detectar quando os clientes se conectam e desconectam. Por exemplo, para marcar um usuário como off-line quando o cliente dele se desconecta.

Os clientes do Firebase Database oferecem primitivos simples que você usa para gravar no banco de dados quando um cliente se desconecta dos servidores do Firebase Database. Podemos confiar nessas atualizações para limpar os dados quando uma conexão é perdida ou quando ocorre uma falha no cliente porque elas são executadas mesmo quando o cliente se desconecta incorretamente. É possível executar todas as operações de gravação, como configuração, atualização e remoção, após uma desconexão.

Este é um exemplo simples de gravação de dados após a desconexão usando o primitivo onDisconnect:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
let presenceRef = Database.database().reference(withPath: "disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnectSetValue("I disconnected!")

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
FIRDatabaseReference *presenceRef = [[FIRDatabase database] referenceWithPath:@"disconnectmessage"];
// Write a string when this client loses connection
[presenceRef onDisconnectSetValue:@"I disconnected!"];

Como o onDisconnect funciona

Quando você estabelece uma operação onDisconnect(), ela fica no servidor do Firebase Realtime Database. O servidor verifica a segurança para garantir que o usuário possa executar o evento de gravação solicitado e informa ao app se ele for inválido. Em seguida, o servidor monitora a conexão. Se, a qualquer momento, o tempo limite da conexão se esgotar ou ela for ativamente encerrada pelo cliente do Realtime Database, o servidor verificará a segurança mais uma vez para garantir que a operação ainda seja válida e, depois, invocará o evento.

O app usa o retorno de chamada na operação de gravação para garantir que o onDisconnect foi corretamente anexado:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
presenceRef.onDisconnectRemoveValue { error, reference in
  if let error = error {
    print("Could not establish onDisconnect event: \(error)")
  }
}

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[presenceRef onDisconnectRemoveValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *reference) {
  if (error != nil) {
    NSLog(@"Could not establish onDisconnect event: %@", error);
  }
}];

Um evento onDisconnect também pode ser cancelado ao chamar .cancel():

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
presenceRef.onDisconnectSetValue("I disconnected")
// some time later when we change our minds
presenceRef.cancelDisconnectOperations()

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
[presenceRef onDisconnectSetValue:@"I disconnected"];
// some time later when we change our minds
[presenceRef cancelDisconnectOperations];

Como detectar um estado de conexão

Para muitos recursos relacionados à presença, é útil que o app saiba quando está on-line ou off-line. O Firebase Realtime Database fornece um local especial em /.info/connected, que é atualizado sempre que o estado da conexão do cliente do Firebase Realtime Database muda. Veja um exemplo:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
let connectedRef = Database.database().reference(withPath: ".info/connected")
connectedRef.observe(.value, with: { snapshot in
  if snapshot.value as? Bool ?? false {
    print("Connected")
  } else {
    print("Not connected")
  }
})

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"];
[connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  if([snapshot.value boolValue]) {
    NSLog(@"connected");
  } else {
    NSLog(@"not connected");
  }
}];

/.info/connected é um valor booleano não sincronizado entre os clientes do Realtime Database, porque esse valor depende do estado deles. Em outras palavras, se um cliente lê /.info/connected como falso, isso não garante que outro também faça a mesma leitura.

Como gerenciar a latência

Carimbos de data/hora do servidor

Os servidores do Firebase Realtime Database têm um mecanismo para inserir carimbos de data/hora gerados no servidor como dados. Combinado com o onDisconnect, esse recurso é uma maneira fácil e segura de armazenar o horário em que um cliente do Realtime Database se desconectou:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
let userLastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")
userLastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
FIRDatabaseReference *userLastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"];
[userLastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]];

Defasagem horária

Embora firebase.database.ServerValue.TIMESTAMP seja muito mais preciso e indicado para a maioria das operações de leitura/gravação, em algumas situações, é importante estimar a defasagem horária dos clientes em relação aos servidores do Firebase Realtime Database. Anexe um callback ao local /.info/serverTimeOffset para receber o valor, em milissegundos, que os clientes do Firebase Realtime Database adicionam ao horário local informado (tempo de época em milissegundos) para estimar o horário do servidor. A precisão dessa diferença pode ser afetada pela latência da rede. Portanto, ela é útil principalmente para descobrir grandes discrepâncias de mais de um segundo no horário do relógio.

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
let offsetRef = Database.database().reference(withPath: ".info/serverTimeOffset")
offsetRef.observe(.value, with: { snapshot in
  if let offset = snapshot.value as? TimeInterval {
    print("Estimated server time in milliseconds: \(Date().timeIntervalSince1970 * 1000 + offset)")
  }
})

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
FIRDatabaseReference *offsetRef = [[FIRDatabase database] referenceWithPath:@".info/serverTimeOffset"];
[offsetRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  NSTimeInterval offset = [(NSNumber *)snapshot.value doubleValue];
  NSTimeInterval estimatedServerTimeMs = [[NSDate date] timeIntervalSince1970] * 1000.0 + offset;
  NSLog(@"Estimated server time: %0.3f", estimatedServerTimeMs);
}];

Amostra de app de presença

Combine as operações de desconexão com o monitoramento de estado da conexão e os carimbos de data/hora do servidor para criar um sistema de presença do usuário. Nesse sistema, cada usuário armazena dados em um local do banco de dados para indicar se o cliente do Realtime Database está on-line. Os clientes definem esse local como verdadeiro quando ficam on-line e um carimbo de data/hora quando se desconectam. Esse carimbo de data/hora indica a última vez em que o usuário esteve on-line.

Observe que o app precisa colocar as operações de desconexão em fila antes que um usuário seja marcado como on-line, para evitar quaisquer disputas se a conexão de rede do cliente for perdida antes que os dois comandos possam ser enviados ao servidor.

Este é um sistema simples de presença do usuário:

Swift

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
// 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
let myConnectionsRef = Database.database().reference(withPath: "users/morgan/connections")

// stores the timestamp of my last disconnect (the last time I was seen online)
let lastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline")

let connectedRef = Database.database().reference(withPath: ".info/connected")

connectedRef.observe(.value, with: { snapshot in
  // only handle connection established (or I've reconnected after a loss of connection)
  guard snapshot.value as? Bool ?? false else { return }

  // add this device to my connections list
  let con = myConnectionsRef.childByAutoId()

  // when this device disconnects, remove it.
  con.onDisconnectRemoveValue()

  // The onDisconnect() call is before the call to set() itself. This is to avoid a race condition
  // where you set the user's presence to true and the client disconnects before the
  // onDisconnect() operation takes effect, leaving a ghost user.

  // this value could contain info about the device or a timestamp instead of just true
  con.setValue(true)

  // when I disconnect, update the last time I was seen online
  lastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())
})

Objective-C

Observação: este produto do Firebase não está disponível no destino Clipes de apps.
// 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
FIRDatabaseReference *myConnectionsRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/connections"];

// stores the timestamp of my last disconnect (the last time I was seen online)
FIRDatabaseReference *lastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"];

FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"];
[connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  if([snapshot.value boolValue]) {
    // connection established (or I've reconnected after a loss of connection)

    // add this device to my connections list
    FIRDatabaseReference *con = [myConnectionsRef childByAutoId];

    // when this device disconnects, remove it
    [con onDisconnectRemoveValue];

    // The onDisconnect() call is before the call to set() itself. This is to avoid a race condition
    // where you set the user's presence to true and the client disconnects before the
    // onDisconnect() operation takes effect, leaving a ghost user.

    // this value could contain info about the device or a timestamp instead of just true
    [con setValue:@YES];


    // when I disconnect, update the last time I was seen online
    [lastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]];
  }
}];