Criar protótipos e fazer testes com o Pacote de emuladores do Firebase (opcional)
Antes de ver informações sobre como um app lê e grava no Realtime Database, confira o Pacote de emuladores do Firebase, um conjunto de ferramentas que podem ser usadas para criar protótipos e testar a funcionalidade do Realtime Database. Conseguir trabalhar localmente sem precisar implantar serviços existentes será uma ótima ideia se você estiver testando diferentes modelos de dados, otimizando suas regras de segurança ou procurando a maneira mais econômica de interagir com o back-end.
Um emulador do Realtime Database faz parte do Pacote de emuladores que permite ao seu app interagir com o conteúdo e a configuração do banco de dados emulado. Além disso, ele também permite interagir com os recursos do projeto emulado (opcional), como funções, outros bancos de dados e regras de segurança.emulator_suite_short
O uso do emulador do Realtime Database envolve apenas algumas etapas:
- Para se conectar ao emulador, adicione uma linha de código à configuração de teste do app.
- Execute
firebase emulators:start
na raiz do diretório do projeto local. - Faça chamadas pelo código de protótipo do app usando o SDK da plataforma do Realtime Database como você faz geralmente ou usando a API REST desse banco de dados.
Veja neste link um tutorial detalhado sobre o Realtime Database e o Cloud Functions. Consulte também a Introdução ao Pacote de emuladores locais.
Receber um DatabaseReference
Para ler ou gravar dados no banco de dados, é necessário uma instância de
DatabaseReference
:
DatabaseReference ref = FirebaseDatabase.instance.ref();
Gravar dados
Este documento aborda as noções básicas de leitura e gravação de dados do Firebase.
Os dados do Firebase são gravados em um DatabaseReference
e recuperados
aguardando ou detectando eventos emitidos pela referência. Os eventos são emitidos
uma vez para o estado inicial dos dados e novamente quando eles são alterados.
Operações básicas de gravação
Em operações básicas de gravação, use set()
para salvar dados em uma referência
específica e substituir os dados no caminho. Você pode definir uma referência
para os seguintes tipos: String
, boolean
, int
, double
, Map
, List
.
Por exemplo, é possível 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"
}
});
O uso de set()
dessa maneira substitui os dados no local especificado,
incluindo qualquer nó filho. No entanto, ainda é possível atualizar um filho sem
substituir o objeto inteiro. Para que os usuários atualizem os próprios perfis,
atualize o nome deles desta forma:
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 os nós, permitindo que você atualize 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
Ler dados detectando eventos de valor
Para ler dados em um caminho e detectar alterações, use a propriedade
onValue
de DatabaseReference
para detectar
DatabaseEvent
s.
É possível usar DatabaseEvent
para ler os dados em um caminho específico,
exatamente como estão no momento do evento. Esse método
será acionado uma vez quando o listener for anexado e sempre que os dados forem alterados,
incluindo os filhos. O evento tem uma propriedade snapshot
que contém
todos os dados no local, incluindo dados filhos. Se não houver dados, a propriedade
exists
do snapshot será false
e value
será nula.
O exemplo a seguir mostra um app de blog social recuperando 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 listener recebe um DataSnapshot
que contém os dados no local
especificado no banco de dados no momento do evento na propriedade value
.
Ler dados uma vez
Ler uma vez usando get()
O SDK foi projetado para gerenciar interações com servidores de banco de dados, independentemente de seu app estar on-line ou off-line.
Geralmente, é necessário usar as técnicas de eventos de valores descritas acima para ler dados para receber notificações de atualizações dos 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 você precisar dos dados apenas uma vez, poderá usar get()
para acessar um snapshot dos
dados do banco de dados. Se por algum motivo o get()
não conseguir retornar o
valor do servidor, o cliente procurará o cache de armazenamento local e retornará um erro
se o valor ainda não for encontrado.
Veja no exemplo a seguir a recuperação do nome de usuário público de um usuário uma única vez no 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 do get()
pode aumentar o uso da largura de banda e causar a perda
de desempenho, o que pode ser impedido com o uso de um listener em tempo real, como mostrado
acima.
Ler dados uma vez com uma vez()
Em alguns casos, é melhor que o valor do cache local seja retornado
imediatamente, em vez de você ter que verificar um valor atualizado no servidor. Nesses
casos, use once()
para receber os dados do cache de disco local
imediatamente.
Isso é útil para dados que só precisam ser carregados uma vez, não são alterados com frequência nem exigem detecção ativa. Por exemplo, o app de blog dos exemplos anteriores usa esse método para carregar o perfil de um usuário quando ele começa a escrever uma nova postagem:
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
Atualização ou exclusão de 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()
, atualize valores de filhos de nível inferior ao
especificar um caminho para a chave. Se os dados estiverem armazenados em vários locais para aprimorar a escalonabilidade, atualize todas as instâncias usando a distribuição de dados. Por exemplo, um app de blog social pode criar uma postagem e atualizá-la simultaneamente no feed de atividades recentes e no feed do autor da postagem. Para fazer isso, o
aplicativo de blog usa estes códigos:
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);
}
Esse exemplo usa push()
para criar uma postagem no nó que armazena as postagens
para todos os usuários em /posts/$postid
e, simultaneamente, recuperar a chave com
key
. Depois, a chave pode ser usada para criar uma segunda entrada nas postagens
do usuário em /user-posts/$userid/$postid
.
Com esses caminhos, você faz atualizações simultâneas em vários locais
da árvore JSON com uma única chamada ao update()
, da mesma forma que esse exemplo
cria a nova postagem nos dois locais. Essas atualizações
são atômicas: ou todas funcionam ou todas falham.
Adicionar um retorno de chamada de conclusão
Para saber quando seus dados foram confirmados, registre
os callbacks de conclusão. set()
e update()
retornam Future
s, aos quais
você pode anexar callbacks de sucesso e erro que são chamados quando a gravação é
confirmada no banco de dados e quando a chamada não é 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 os dados é chamar remove()
em uma referência ao
local desses dados.
Também é possível fazer a exclusão ao especificar nulo como o valor de outra operação
de gravação, como set()
ou update()
. Use essa 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, use uma transação transmitindo um
gerenciador de transações para runTransaction()
. Um gerenciador de transações usa o
estado atual dos dados como um argumento e
retorna o novo estado desejado de acordo com suas preferências. Se
outro cliente fizer uma gravação no local antes que seu novo valor seja gravado
com sucesso, sua função de atualização será chamada novamente com o novo valor atual e
a gravação será repetida.
Por exemplo, os usuários do app de blog social podem adicionar ou remover estrelas de postagens e acompanhar quantas foram recebidas 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 sempre que a função de atualização de transação é executada.
Portanto, você executa a função várias vezes, podendo ver estados intermediários.
É possível definir applyLocally
como false
para suprimir esses estados intermediários e
esperar 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, por exemplo, se a transação foi confirmada e o novo snapshot:
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
Cancelar uma transação
Se você quiser cancelar uma transação com segurança, chame Transaction.abort()
para
gerar 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 a publicação com estrela ou remove a marcação e a contagem de estrelas incrementada. Se já soubermos que o usuário está marcando a postagem com estrela, poderemos 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, ele não será executado automaticamente se houver uma atualização conflitante. No entanto, como a operação de incremento acontece diretamente no servidor de banco de dados, não há possibilidade de conflito.
Se você quiser detectar e rejeitar conflitos específicos do aplicativo, como um usuário marcando uma postagem que já havia marcado antes, escreva regras de segurança personalizadas para esse caso de uso.
Trabalhar com dados off-line
Se um cliente perder a conexão de rede, o app continuará funcionando.
Todos os clientes conectados a um banco de dados do Firebase mantêm a própria versão interna de dados ativos. A gravação deles ocorre primeiro nessa versão local. Depois, o cliente do Firebase sincroniza esses dados com os servidores remotos e com outros clientes de acordo com o modelo "melhor esforço".
Consequentemente, todas as gravações no banco de dados acionam eventos locais, antes de qualquer dado ser gravado no servidor, e o app continua responsivo, independentemente da conectividade ou da latência da rede.
Para que a conectividade seja restabelecida, seu app recebe o conjunto apropriado de eventos, e o cliente faz a sincronização com o estado atual do servidor, sem precisar de um código personalizado.
Confira Saiba mais sobre recursos on-line e off-line se você quiser ver detalhes sobre esse assunto.
Próximas etapas
- Como trabalhar com listas de dados
- Saiba como estruturar dados
- Saiba mais sobre recursos on-line e off-line