Criar protótipos e fazer testes com Firebase Local Emulator Suite (opcional)
Antes de compreender como o app lê e grava no Realtime Database, é importante conhecer um conjunto de ferramentas que podem ser usadas para criar protótipos e testar a funcionalidade do Realtime Database: Firebase Local Emulator Suite. 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 Local Emulator Suite, que permite que o app interaja com o conteúdo e a configuração do banco de dados emulado e também com os recursos do projeto emulado (funções, outros bancos de dados e regras de segurança).
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 um SDK da plataforma do Realtime Database, como de costume, ou a API REST do Realtime Database.
Confira um tutorial detalhado sobre o Realtime Database e o Cloud Functions. Consulte também a introdução do Local Emulator Suite.
Como ter uma FIRDatabaseReference
Para ler ou gravar dados no banco de dados, você precisa de uma instância de
FIRDatabaseReference
:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Gravar dados
Veja neste documento as noções básicas de leitura e gravação de dados do Firebase.
Esses dados são gravados em uma referência Database
e recuperados com
um listener assíncrono anexado à referência. Ele é acionado
uma vez no estado inicial dos dados, e posteriormente, quando há alterações.
Operações básicas de gravação
Em operações básicas de gravação, use setValue
para salvar dados em uma referência
específica e substituir os dados no caminho. Use esse método para as seguintes ações:
- Transmitir os tipos que correspondam aos tipos JSON disponíveis:
NSString
NSNumber
NSDictionary
NSArray
Por exemplo, é possível adicionar um usuário com setValue
da seguinte maneira:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
O uso de setValue
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:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Ler dados
Ler dados detectando eventos de valores
Para ler dados em um caminho e detectar as alterações, use o
observeEventType:withBlock
do FIRDatabaseReference
para observar
os eventos FIRDataEventTypeValue
.
Tipo de evento | Uso normal |
---|---|
FIRDataEventTypeValue |
Ler e detectar alterações em todo o conteúdo de um caminho. |
É possível usar o evento FIRDataEventTypeValue
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 houver alteração nos dados, inclusive nos
filhos. O retorno de chamada do evento recebe um snapshot
contendo todos os dados nesse
local, incluindo os dados filhos. Se não houver dados, o snapshot retornará
false
na chamada de exists()
e nil
, quando a propriedade value
é lida.
O exemplo a seguir mostra um aplicativo de blog social recuperando detalhes de uma postagem do banco de dados:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
O listener recebe um FIRDataSnapshot
que contém os dados no local
especificado no banco de dados no momento do evento na propriedade value
. É possível
atribuir os valores ao tipo nativo apropriado, como NSDictionary
.
Se não houver dados no local, o value
será nil
.
Ler dados uma vez
Ler uma vez usando getData()
O SDK foi projetado para gerenciar interações com servidores de banco de dados com seu app 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 o getData()
para acessar um snapshot dos
dados do banco de dados. Se por algum motivo o getData()
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:
Swift
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Objective-C
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid]; [[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) { if (error) { NSLog(@"Received an error %@", error); return; } NSString *userName = snapshot.value; }];
O uso desnecessário de getData()
pode aumentar a utilização da largura de banda e causar a perda
de desempenho, o que pode ser impedido com um listener em tempo real, conforme mostrado
acima.
Ler dados uma vez com um observador
Em alguns casos, é possível que o valor do cache local seja retornado
imediatamente, em vez de verificar um valor atualizado no servidor. Nesses
casos, use observeSingleEventOfType
para receber os dados do
cache do 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 este método para carregar o perfil de um usuário quando ele começa a escrever uma nova postagem:
Swift
let userID = Auth.auth().currentUser?.uid ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in // Get user value let value = snapshot.value as? NSDictionary let username = value?["username"] as? String ?? "" let user = User(username: username) // ... }) { error in print(error.localizedDescription) }
Objective-C
NSString *userID = [FIRAuth auth].currentUser.uid; [[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { // Get user value User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]]; // ... } withCancelBlock:^(NSError * _Nonnull error) { NSLog(@"%@", error.localizedDescription); }];
Atualizar ou excluir 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 updateChildValues
.
Ao chamar updateChildValues
, 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:
Swift
guard let key = ref.child("posts").childByAutoId().key else { return } let post = ["uid": userID, "author": username, "title": title, "body": body] let childUpdates = ["/posts/\(key)": post, "/user-posts/\(userID)/\(key)/": post] ref.updateChildValues(childUpdates)
Objective-C
NSString *key = [[_ref child:@"posts"] childByAutoId].key; NSDictionary *post = @{@"uid": userID, @"author": username, @"title": title, @"body": body}; NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post, [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post}; [_ref updateChildValues:childUpdates];
Esse exemplo usa childByAutoId
para criar uma postagem no nó que armazena as postagens para
todos os usuários em /posts/$postid
e, simultaneamente, recuperar a chave com
getKey()
. 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 updateChildValues
, 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 bloco de conclusão
Para saber quando seus dados foram confirmados, você pode adicionar um
bloco de conclusão. setValue
e updateChildValues
recebem um bloco de conclusão opcional que é chamado quando a gravação é confirmada no banco de dados. Esse listener pode ser útil para monitorar quais dados foram salvos e quais dados ainda estão sendo sincronizados. Se a chamada não for bem-sucedida, o listener receberá um objeto de erro indicando o motivo da falha.
Swift
do { try await ref.child("users").child(user.uid).setValue(["username": username]) print("Data saved successfully!") } catch { print("Data could not be saved: \(error).") }
Objective-C
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { if (error) { NSLog(@"Data could not be saved: %@", error); } else { NSLog(@"Data saved successfully."); } }];
Excluir dados
A maneira mais simples de excluir os dados é chamar removeValue
em uma referência ao
local desses dados.
Também é possível fazer a exclusão ao especificar nil
como o valor de outra operação
de gravação, como setValue
ou updateChildValues
. Você pode usar essa técnica com updateChildValues
para excluir vários filhos em uma única chamada de API.
Remover listeners
Os observadores não interrompem automaticamente a sincronização de dados quando você sai de um
ViewController
. Se um observador não for removido corretamente, ele continuará sincronizando
dados com a memória local. Quando um observador não for mais necessário, remova-o passando
o FIRDatabaseHandle
associado ao método removeObserverWithHandle
.
Ao adicionar um bloco de retorno de chamada a uma referência, um FIRDatabaseHandle
é retornado.
Esses handles podem ser usados para remover esse bloco.
Se vários listeners são adicionados a uma referência de banco de dados, cada um é
chamado quando um evento é acionado. Para interromper a sincronização de dados nesse local, remova todos os observadores em um local chamando o método removeAllObservers
.
Chamar removeObserverWithHandle
ou removeAllObservers
em um listener não remove automaticamente os listeners registrados nos nós filhos. É preciso também encontrar essas referências ou handles para removê-los.
Salvar dados como transações
Ao trabalhar com dados que podem ser corrompidos por modificações simultâneas, como contadores incrementais, use uma operação de transação. Essa operação aceita dois argumentos: uma função de atualização e um callback de conclusão opcional. A função de atualização usa o estado atual dos dados como argumento e retorna o novo estado de acordo com as preferências de gravação.
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:
Swift
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in if var post = currentData.value as? [String: AnyObject], let uid = Auth.auth().currentUser?.uid { var stars: [String: Bool] stars = post["stars"] as? [String: Bool] ?? [:] var starCount = post["starCount"] as? Int ?? 0 if let _ = stars[uid] { // Unstar the post and remove self from stars starCount -= 1 stars.removeValue(forKey: uid) } else { // Star the post and add self to stars starCount += 1 stars[uid] = true } post["starCount"] = starCount as AnyObject? post["stars"] = stars as AnyObject? // Set value and report transaction success currentData.value = post return TransactionResult.success(withValue: currentData) } return TransactionResult.success(withValue: currentData) }) { error, committed, snapshot in if let error = error { print(error.localizedDescription) } }
Objective-C
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { NSMutableDictionary *post = currentData.value; if (!post || [post isEqual:[NSNull null]]) { return [FIRTransactionResult successWithValue:currentData]; } NSMutableDictionary *stars = post[@"stars"]; if (!stars) { stars = [[NSMutableDictionary alloc] initWithCapacity:1]; } NSString *uid = [FIRAuth auth].currentUser.uid; int starCount = [post[@"starCount"] intValue]; if (stars[uid]) { // Unstar the post and remove self from stars starCount--; [stars removeObjectForKey:uid]; } else { // Star the post and add self to stars starCount++; stars[uid] = @YES; } post[@"stars"] = stars; post[@"starCount"] = @(starCount); // Set value and report transaction success currentData.value = post; return [FIRTransactionResult successWithValue:currentData]; } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { // Transaction completed if (error) { NSLog(@"%@", error.localizedDescription); } }];
A transação impede uma contagem incorreta de estrelas se vários usuários adicionam simultaneamente estrelas à mesma postagem ou se o cliente tem dados desatualizados. Inicialmente, o valor contido na classe FIRMutableData
é o último conhecido pelo cliente para o caminho, ou nil
se não houver nenhum. O servidor compara o
valor inicial com o atual e aceita a transação se
eles forem correspondentes. Caso contrário, ele a rejeita. Se a transação é rejeitada, o servidor retorna o valor atual para o cliente, que executa a transação novamente com o valor atualizado. Isso se repetirá até que a transação seja aceita ou até que muitas tentativas sejam realizadas.
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, podemos usar uma operação de incremento atômico em vez de uma transação.
Swift
let updates = [ "posts/\(postID)/stars/\(userID)": true, "posts/\(postID)/starCount": ServerValue.increment(1), "user-posts/\(postID)/stars/\(userID)": true, "user-posts/\(postID)/starCount": ServerValue.increment(1) ] as [String : Any] Database.database().reference().updateChildValues(updates)
Objective-C
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1], [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]}; [[[FIRDatabase database] reference] updateChildValues:updates];
Este código não usa uma operação de transação. Portanto, ele não será executado de novo 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 adequado 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