Ler e gravar dados

(Opcional) Protótipo e teste com Firebase Emulator Suite

Antes de falar sobre como seu aplicativo lê e grava no Realtime Database, vamos apresentar um conjunto de ferramentas que você pode usar para criar protótipos e testar a funcionalidade do Realtime Database: Firebase Emulator Suite. Se você estiver experimentando diferentes modelos de dados, otimizando suas regras de segurança ou trabalhando para encontrar a maneira mais econômica de interagir com o back-end, poder trabalhar localmente sem implantar serviços ativos pode ser uma ótima ideia.

Um emulador do Realtime Database faz parte do Emulator Suite, que permite que seu aplicativo interaja com o conteúdo e a configuração do banco de dados emulado, bem como, opcionalmente, com os recursos do projeto emulado (funções, outros bancos de dados e regras de segurança).emulator_suite_short

Usar o emulador do Realtime Database envolve apenas algumas etapas:

  1. Adicionando uma linha de código à configuração de teste do seu aplicativo para conectar-se ao emulador.
  2. Na raiz do diretório local do projeto, executando firebase emulators:start .
  3. Fazer chamadas a partir do código do protótipo do seu aplicativo usando um SDK da plataforma Realtime Database normalmente ou usando a API REST do Realtime Database.

Um passo a passo detalhado envolvendo o Realtime Database e o Cloud Functions está disponível. Você também deve dar uma olhada na introdução do Emulator Suite .

Obtenha uma referência de banco de dados

Para ler ou gravar dados do banco de dados, você precisa de uma instância de DatabaseReference :

DatabaseReference ref = FirebaseDatabase.instance.ref();

Gravar dados

Este documento aborda os conceitos básicos de leitura e gravação de dados do Firebase.

Os dados do Firebase são gravados em um DatabaseReference e recuperados aguardando ou ouvindo eventos emitidos pela referência. Os eventos são emitidos uma vez para o estado inicial dos dados e novamente sempre que os dados são alterados.

Operações básicas de gravação

Para operações básicas de gravação, você pode usar set() para salvar dados em uma referência especificada, substituindo quaisquer dados existentes nesse caminho. Você pode definir uma referência para os seguintes tipos: String , boolean , int , double , Map , List .

Por exemplo, você pode adicionar um usuário com set() da seguinte maneira:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

Usar set() dessa forma substitui os dados no local especificado, incluindo quaisquer nós filhos. No entanto, você ainda pode atualizar um filho sem reescrever o objeto inteiro. Se quiser permitir que os usuários atualizem seus perfis, você pode atualizar o nome de usuário da seguinte maneira:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the name, leave the age and address!
await ref.update({
  "age": 19,
});

O método update() aceita um subcaminho para nós, permitindo atualizar vários nós no banco de dados de uma só vez:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

Ler dados

Leia dados ouvindo eventos de valor

Para ler dados em um caminho e escutar alterações, use a propriedade onValue de DatabaseReference para escutar DatabaseEvent s.

Você pode usar o DatabaseEvent para ler os dados em um determinado caminho, conforme eles existem no momento do evento. Este evento é acionado uma vez quando o ouvinte é anexado e novamente sempre que os dados, incluindo quaisquer filhos, são alterados. O evento possui uma propriedade snapshot contendo todos os dados naquele local, incluindo dados filho. Se não houver dados, a propriedade exists do instantâneo será false e sua propriedade value será nula.

O exemplo a seguir demonstra um aplicativo de blog social recuperando os detalhes de uma postagem do banco de dados:

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

O ouvinte recebe um DataSnapshot que contém os dados no local especificado no banco de dados no momento do evento em sua propriedade value .

Leia os dados uma vez

Leia uma vez usando get()

O SDK foi projetado para gerenciar interações com servidores de banco de dados, esteja seu aplicativo online ou offline.

Geralmente, você deve usar as técnicas de eventos de valor descritas acima para ler dados e ser notificado sobre atualizações nos dados do back-end. Essas técnicas reduzem o uso e o faturamento e são otimizadas para oferecer aos usuários a melhor experiência on-line e off-line.

Se precisar dos dados apenas uma vez, você pode usar get() para obter um instantâneo dos dados do banco de dados. Se por algum motivo get() não conseguir retornar o valor do servidor, o cliente investigará o cache de armazenamento local e retornará um erro se o valor ainda não for encontrado.

O exemplo a seguir demonstra a recuperação do nome de usuário público de um usuário uma única vez do banco de dados:

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

O uso desnecessário de get() pode aumentar o uso da largura de banda e levar à perda de desempenho, o que pode ser evitado usando um ouvinte em tempo real conforme mostrado acima.

Leia os dados uma vez com once()

Em alguns casos você pode querer que o valor do cache local seja retornado imediatamente, em vez de verificar um valor atualizado no servidor. Nesses casos você pode usar once() para obter os dados do cache do disco local imediatamente.

Isso é útil para dados que só precisam ser carregados uma vez e não se espera que sejam alterados com frequência ou que exijam escuta ativa. Por exemplo, o aplicativo de blog dos exemplos anteriores usa este método para carregar o perfil de um usuário quando ele começa a criar uma nova postagem:

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

Atualizando ou excluindo dados

Atualizar campos específicos

Para gravar simultaneamente em filhos específicos de um nó sem substituir outros nós filhos, use o método update() .

Ao chamar update() , você pode atualizar valores filho de nível inferior especificando um caminho para a chave. Se os dados forem armazenados em vários locais para melhor escalar, você poderá atualizar todas as instâncias desses dados usando o data fan-out . Por exemplo, um aplicativo de blog social pode querer criar uma postagem e atualizá-la simultaneamente para o feed de atividades recentes e para o feed de atividades do usuário da postagem. Para fazer isso, o aplicativo de blog usa um código como este:

void writeNewPost(String uid, String username, String picture, String title,
        String body) async {
    // A post entry.
    final postData = {
        'author': username,
        'uid': uid,
        'body': body,
        'title': title,
        'starCount': 0,
        'authorPic': picture,
    };

    // Get a key for a new Post.
    final newPostKey =
        FirebaseDatabase.instance.ref().child('posts').push().key;

    // Write the new post's data simultaneously in the posts list and the
    // user's post list.
    final Map<String, Map> updates = {};
    updates['/posts/$newPostKey'] = postData;
    updates['/user-posts/$uid/$newPostKey'] = postData;

    return FirebaseDatabase.instance.ref().update(updates);
}

Este exemplo usa push() para criar uma postagem no nó contendo postagens para todos os usuários em /posts/$postid e simultaneamente recuperar a chave com key . A chave pode então ser usada para criar uma segunda entrada nas postagens do usuário em /user-posts/$userid/$postid .

Usando esses caminhos, você pode realizar atualizações simultâneas em vários locais na árvore JSON com uma única chamada para update() , como este exemplo cria a nova postagem em ambos os locais. As atualizações simultâneas feitas dessa maneira são atômicas: ou todas as atualizações são bem-sucedidas ou todas as atualizações falham.

Adicione um retorno de chamada de conclusão

Se quiser saber quando seus dados foram confirmados, você pode registrar retornos de chamada de conclusão. Tanto set() quanto update() retornam Future s, aos quais você pode anexar retornos de chamada de sucesso e erro que são chamados quando a gravação foi confirmada no banco de dados e quando a chamada não foi bem-sucedida.

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

Excluir dados

A maneira mais simples de excluir dados é chamar remove() em uma referência à localização desses dados.

Você também pode excluir especificando null como o valor para outra operação de gravação, como set() ou update() . Você pode usar esta técnica com update() para excluir vários filhos em uma única chamada de API.

Salvar dados como transações

Ao trabalhar com dados que podem ser corrompidos por modificações simultâneas, como contadores incrementais, você pode usar uma transação passando um manipulador de transação para runTransaction() . Um manipulador de transação toma o estado atual dos dados como argumento e retorna o novo estado desejado que você gostaria de escrever. Se outro cliente gravar no local antes que seu novo valor seja gravado com êxito, sua função de atualização será chamada novamente com o novo valor atual e a gravação será repetida.

Por exemplo, no aplicativo de blog social de exemplo, você pode permitir que os usuários marquem e desmarquem postagens com estrela e controlem quantas estrelas uma postagem recebeu da seguinte maneira:

void toggleStar(String uid) async {
  DatabaseReference postRef =
      FirebaseDatabase.instance.ref("posts/foo-bar-123");

  TransactionResult result = await postRef.runTransaction((Object? post) {
    // Ensure a post at the ref exists.
    if (post == null) {
      return Transaction.abort();
    }

    Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
    if (_post["stars"] is Map && _post["stars"][uid] != null) {
      _post["starCount"] = (_post["starCount"] ?? 1) - 1;
      _post["stars"][uid] = null;
    } else {
      _post["starCount"] = (_post["starCount"] ?? 0) + 1;
      if (!_post.containsKey("stars")) {
        _post["stars"] = {};
      }
      _post["stars"][uid] = true;
    }

    // Return the new data.
    return Transaction.success(_post);
  });
}

Por padrão, os eventos são gerados cada vez que a função de atualização da transação é executada, portanto, se você executar a função várias vezes, poderá ver estados intermediários. Você pode definir applyLocally como false para suprimir esses estados intermediários e, em vez disso, aguardar até que a transação seja concluída antes que os eventos sejam gerados:

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

O resultado de uma transação é um TransactionResult , que contém informações como se a transação foi confirmada e o novo instantâneo:

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");

TransactionResult result = await ref.runTransaction((Object? post) {
  // ...
});

print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

Cancelando uma transação

Se você quiser cancelar uma transação com segurança, chame Transaction.abort() para lançar uma AbortTransactionException :

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

Incrementos atômicos do lado do servidor

No caso de uso acima, estamos gravando dois valores no banco de dados: o ID do usuário que marca/desmarca a postagem com estrela e a contagem incrementada de estrelas. Se já sabemos que o usuário está estrelando a postagem, podemos usar uma operação de incremento atômico em vez de uma transação.

void addStar(uid, key) async {
  Map<String, Object?> updates = {};
  updates["posts/$key/stars/$uid"] = true;
  updates["posts/$key/starCount"] = ServerValue.increment(1);
  updates["user-posts/$key/stars/$uid"] = true;
  updates["user-posts/$key/starCount"] = ServerValue.increment(1);
  return FirebaseDatabase.instance.ref().update(updates);
}

Este código não usa uma operação de transação, portanto, não será executado novamente se houver uma atualização conflitante. Porém, como a operação de incremento acontece diretamente no servidor de banco de dados, não há chance de conflito.

Se você quiser detectar e rejeitar conflitos específicos do aplicativo, como um usuário marcando uma postagem que já marcou com estrela antes, você deverá escrever regras de segurança personalizadas para esse caso de uso.

Trabalhe com dados off-line

Se um cliente perder a conexão de rede, seu aplicativo continuará funcionando corretamente.

Cada cliente conectado a um banco de dados Firebase mantém sua própria versão interna de quaisquer dados ativos. Quando os dados são gravados, eles são gravados primeiro nesta versão local. O cliente Firebase então sincroniza esses dados com os servidores de banco de dados remotos e com outros clientes com base no "melhor esforço".

Como resultado, todas as gravações no banco de dados acionam eventos locais imediatamente, antes que qualquer dado seja gravado no servidor. Isso significa que seu aplicativo permanece responsivo independentemente da latência ou conectividade da rede.

Depois que a conectividade for restabelecida, seu aplicativo receberá o conjunto apropriado de eventos para que o cliente sincronize com o estado atual do servidor, sem precisar escrever nenhum código personalizado.

Falaremos mais sobre o comportamento offline em Saiba mais sobre recursos online e offline .

Próximos passos