Habilitando recursos off-line

Os aplicativos do Firebase funcionam mesmo se o aplicativo perder temporariamente a conexão de rede. Além disso, o Firebase fornece ferramentas para persistir dados localmente, gerenciar presença e lidar com latência.

Persistência de disco

Os aplicativos Firebase lidam automaticamente com interrupções temporárias de rede. Os dados armazenados em cache ficam disponíveis off-line e o Firebase reenvia qualquer gravação quando a conectividade de rede é restaurada.

Quando você habilita a persistência de disco, seu aplicativo grava os dados localmente no dispositivo para que ele possa manter o estado off-line, mesmo que o usuário ou o sistema operacional reinicie o aplicativo.

Você pode ativar a persistência do disco com apenas uma linha de código.

FirebaseDatabase.instance.setPersistenceEnabled(true);

Comportamento de Persistência

Ao ativar a persistência, todos os dados que o cliente do Firebase Realtime Database sincronizaria enquanto on-line persistiriam no disco e ficariam disponíveis off-line, mesmo quando o usuário ou o sistema operacional reiniciasse o aplicativo. Isso significa que seu aplicativo funciona como funcionaria online, usando os dados locais armazenados no cache. Os retornos de chamada do ouvinte continuarão a ser acionados para 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 aplicativo está off-line. Quando a persistência está habilitada, essa fila também é persistida no disco para que todas as suas gravações estejam disponíveis quando o usuário ou o sistema operacional reiniciar o aplicativo. Quando o aplicativo recupera a conectividade, todas as operações são enviadas para o servidor Firebase Realtime Database.

Se o seu aplicativo usar o Firebase Authentication , o cliente do Firebase Realtime Database manterá o token de autenticação do usuário durante as reinicializações do aplicativo. Se o token de autenticação expirar enquanto seu aplicativo estiver offline, o cliente pausará as operações de gravação até que seu aplicativo autentica novamente o usuário, caso contrário, as operações de gravação poderão falhar devido a regras de segurança.

Mantendo os dados atualizados

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

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

O cliente do Firebase Realtime Database baixa automaticamente os dados nesses locais e os mantém sincronizados mesmo que a referência não tenha listeners ativos. Você pode desativar a sincronização com a seguinte linha de código.

scoresRef.keepSynced(false);

Por padrão, 10 MB de dados sincronizados anteriormente são armazenados em cache. Isso deve ser suficiente para a maioria das aplicações. Se o cache ultrapassar o tamanho configurado, o Firebase Realtime Database limpará os dados que foram usados ​​menos recentemente. Os dados mantidos sincronizados não são eliminados do cache.

Consultando dados off-line

O Firebase Realtime Database armazena dados retornados de uma consulta para uso off-line. Para consultas construídas off-line, o Firebase Realtime Database continua funcionando para dados carregados anteriormente. Se os dados solicitados não forem carregados, o Firebase Realtime Database carrega os dados do cache local. Quando a conectividade de rede estiver disponível novamente, os dados serão carregados e refletirão a consulta.

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

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}.");
});

Suponha que o usuário perca a conexão, fique offline e reinicie o aplicativo. Enquanto ainda está off-line, o aplicativo consulta os dois últimos itens do mesmo local. Esta consulta retornará com êxito os dois últimos itens porque o aplicativo carregou todos os quatro itens da consulta acima.

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

No exemplo anterior, o cliente do Firebase Realtime Database gera eventos 'filho adicionado' para os dois dinossauros com pontuação mais alta, usando o cache persistente. Mas não gerará um evento de 'valor', pois o aplicativo nunca executou essa consulta enquanto estava online.

Se o aplicativo solicitasse os últimos seis itens enquanto estava off-line, ele obteria eventos de 'criança adicionada' para os quatro itens armazenados em cache imediatamente. Quando o dispositivo fica on-line novamente, o cliente do Firebase Realtime Database sincroniza com o servidor e obtém os dois eventos finais de 'filho adicionado' e de 'valor' para o aplicativo.

Tratamento de transações off-line

Todas as transações realizadas enquanto o aplicativo está offline serão enfileiradas. Depois que o aplicativo recuperar a conectividade de rede, as transações serão enviadas ao servidor do Realtime Database.

O Firebase Realtime Database possui muitos recursos para lidar com cenários off-line e conectividade de rede. O restante deste guia se aplica ao seu aplicativo, independentemente de você ter a persistência habilitada ou não.

Gerenciando Presença

Em aplicações em tempo real, muitas vezes é útil detectar quando os clientes se conectam e desconectam. Por exemplo, você pode querer marcar um usuário como 'offline' quando seu cliente se desconectar.

Os clientes do Firebase Database fornecem primitivos simples que você pode usar para gravar no banco de dados quando um cliente se desconecta dos servidores do Firebase Database. Essas atualizações ocorrem independentemente de o cliente se desconectar corretamente ou não, portanto, você pode contar com elas para limpar os dados mesmo se uma conexão for interrompida ou um cliente travar. Todas as operações de gravação, incluindo configuração, atualização e remoção, podem ser executadas após uma desconexão.

Aqui está um exemplo simples de gravação de dados após a desconexão usando a primitiva onDisconnect :

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

Como funciona o onDisconnect

Quando você estabelece uma operação onDisconnect() , a operação reside no servidor 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 seu aplicativo se ele for inválido. O servidor então monitora a conexão. Se em algum momento a conexão atingir o tempo limite ou for fechada ativamente pelo cliente do Realtime Database, o servidor verificará a segurança uma segunda vez (para garantir que a operação ainda seja válida) e, em seguida, invocará o evento.

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

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

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

Detectando o estado da conexão

Para muitos recursos relacionados à presença, é útil que seu aplicativo saiba quando está online ou offline. 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. Aqui está um exemplo:

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 é um valor booleano que não é sincronizado entre clientes do Realtime Database porque o valor depende do estado do cliente. Em outras palavras, se um cliente ler /.info/connected como falso, isso não garante que um cliente separado também lerá falso.

Lidando com latência

Carimbos de data e hora do servidor

Os servidores do Firebase Realtime Database fornecem um mecanismo para inserir carimbos de data/hora gerados no servidor como dados. Esse recurso, combinado com onDisconnect , fornece uma maneira fácil de anotar com segurança o momento em que um cliente do Realtime Database se desconectou:

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

Inclinação do relógio

Embora ServerValue.timestamp seja muito mais preciso e preferível para a maioria das operações de leitura/gravação, ocasionalmente pode ser útil estimar a distorção do relógio do cliente em relação aos servidores do Firebase Realtime Database. Você pode anexar um retorno de chamada ao local /.info/serverTimeOffset para obter o valor, em milissegundos, que os clientes do Firebase Realtime Database adicionam ao horário local relatado (tempo de época em milissegundos) para estimar o horário do servidor. Observe que a precisão desse deslocamento pode ser afetada pela latência da rede e, portanto, é útil principalmente para descobrir grandes discrepâncias (> 1 segundo) no tempo do relógio.

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;
});

Exemplo de aplicativo de presença

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

Observe que seu aplicativo deve enfileirar as operações de desconexão antes que um usuário seja marcado como online, para evitar quaisquer condições de corrida caso a conexão de rede do cliente seja perdida antes que ambos os comandos possam ser enviados ao servidor.

// 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);
  }
});