Este documento aborda os conceitos básicos de leitura e gravação de dados do Firebase.
Os dados do Firebase são gravados em uma referência FirebaseDatabase
e recuperados anexando um listener assíncrono à referência. O ouvinte é acionado uma vez para o estado inicial dos dados e novamente sempre que os dados são alterados.
(Opcional) Protótipo e teste com Firebase Local 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 Local 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 Local 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).
Usar o emulador do Realtime Database envolve apenas algumas etapas:
- Adicionando uma linha de código à configuração de teste do seu aplicativo para conectar-se ao emulador.
- Na raiz do diretório local do projeto, executando
firebase emulators:start
. - 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 Local 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
:
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Gravar dados
Operações básicas de gravação
Para operações básicas de gravação, você pode usar setValue()
para salvar dados em uma referência especificada, substituindo quaisquer dados existentes nesse caminho. Você pode usar este método para:
- Passe tipos que correspondem aos tipos JSON disponíveis da seguinte maneira:
-
String
-
Long
-
Double
-
Boolean
-
Map<String, Object>
-
List<Object>
-
- Passe um objeto Java personalizado, se a classe que o define tiver um construtor padrão que não aceita argumentos e possui getters públicos para as propriedades a serem atribuídas.
Se você usar um objeto Java, o conteúdo do seu objeto será automaticamente mapeado para locais filho de forma aninhada. Usar um objeto Java normalmente também torna seu código mais legível e mais fácil de manter. Por exemplo, se você tiver um aplicativo com um perfil de usuário básico, seu objeto User
poderá ter a seguinte aparência:
Kotlin+KTX
@IgnoreExtraProperties data class User(val username: String? = null, val email: String? = null) { // Null default values create a no-argument default constructor, which is needed // for deserialization from a DataSnapshot. }
Java
@IgnoreExtraProperties public class User { public String username; public String email; public User() { // Default constructor required for calls to DataSnapshot.getValue(User.class) } public User(String username, String email) { this.username = username; this.email = email; } }
Você pode adicionar um usuário com setValue()
da seguinte maneira:
Kotlin+KTX
fun writeNewUser(userId: String, name: String, email: String) { val user = User(name, email) database.child("users").child(userId).setValue(user) }
Java
public void writeNewUser(String userId, String name, String email) { User user = new User(name, email); mDatabase.child("users").child(userId).setValue(user); }
Usar setValue()
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:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Ler dados
Ler dados com ouvintes persistentes
Para ler dados em um caminho e ouvir alterações, use o método addValueEventListener()
para adicionar um ValueEventListener
a um DatabaseReference
.
Ouvinte | Retorno de chamada do evento | Uso típico |
---|---|---|
ValueEventListener | onDataChange() | Leia e ouça alterações em todo o conteúdo de um caminho. |
Você pode usar o método onDataChange()
para ler um instantâneo estático do conteúdo em um determinado caminho, conforme ele existia no momento do evento. Este método é acionado uma vez quando o ouvinte é anexado e novamente sempre que os dados, incluindo filhos, são alterados. O retorno de chamada do evento recebe um instantâneo contendo todos os dados naquele local, incluindo dados filho. Se não houver dados, o instantâneo retornará false
quando você chamar exists()
e null
quando você chamar getValue()
nele.
O exemplo a seguir demonstra um aplicativo de blog social recuperando os detalhes de uma postagem do banco de dados:
Kotlin+KTX
val postListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { // Get Post object and use the values to update the UI val post = dataSnapshot.getValue<Post>() // ... } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) } } postReference.addValueEventListener(postListener)
Java
ValueEventListener postListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // Get Post object and use the values to update the UI Post post = dataSnapshot.getValue(Post.class); // .. } @Override public void onCancelled(DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); } }; mPostReference.addValueEventListener(postListener);
O ouvinte recebe um DataSnapshot
que contém os dados no local especificado no banco de dados no momento do evento. Chamar getValue()
em um snapshot retorna a representação do objeto Java dos dados. Se não existirem dados no local, chamar getValue()
retornará null
.
Neste exemplo, ValueEventListener
também define o método onCancelled()
que será chamado se a leitura for cancelada. Por exemplo, uma leitura poderá ser cancelada se o cliente não tiver permissão para ler em um local de banco de dados do Firebase. Este método recebe um objeto DatabaseError
indicando o motivo da falha.
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 ValueEventListener
descritas acima para ler dados e ser notificado sobre atualizações nos dados do back-end. As técnicas de ouvinte 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 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.
Kotlin+KTX
mDatabase.child("users").child(userId).get().addOnSuccessListener {
Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
Log.e("firebase", "Error getting data", it)
}
Java
mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
@Override
public void onComplete(@NonNull Task<DataSnapshot> task) {
if (!task.isSuccessful()) {
Log.e("firebase", "Error getting data", task.getException());
}
else {
Log.d("firebase", String.valueOf(task.getResult().getValue()));
}
}
});
Leia uma vez usando um ouvinte
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 addListenerForSingleValueEvent
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 esse método para carregar o perfil de um usuário quando ele começa a criar uma nova postagem.
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 updateChildren()
.
Ao chamar updateChildren()
, 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 ter uma classe Post
como esta:
Kotlin+KTX
@IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", var body: String? = "", var starCount: Int = 0, var stars: MutableMap<String, Boolean> = HashMap(), ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, "body" to body, "starCount" to starCount, "stars" to stars, ) } }
Java
@IgnoreExtraProperties public class Post { public String uid; public String author; public String title; public String body; public int starCount = 0; public Map<String, Boolean> stars = new HashMap<>(); public Post() { // Default constructor required for calls to DataSnapshot.getValue(Post.class) } public Post(String uid, String author, String title, String body) { this.uid = uid; this.author = author; this.title = title; this.body = body; } @Exclude public Map<String, Object> toMap() { HashMap<String, Object> result = new HashMap<>(); result.put("uid", uid); result.put("author", author); result.put("title", title); result.put("body", body); result.put("starCount", starCount); result.put("stars", stars); return result; } }
Para criar uma postagem e atualizá-la simultaneamente para o feed de atividades recentes e para o feed de atividades do usuário da postagem, o aplicativo de blog usa um código como este:
Kotlin+KTX
private fun writeNewPost(userId: String, username: String, title: String, body: String) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously val key = database.child("posts").push().key if (key == null) { Log.w(TAG, "Couldn't get push key for posts") return } val post = Post(userId, username, title, body) val postValues = post.toMap() val childUpdates = hashMapOf<String, Any>( "/posts/$key" to postValues, "/user-posts/$userId/$key" to postValues, ) database.updateChildren(childUpdates) }
Java
private void writeNewPost(String userId, String username, String title, String body) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously String key = mDatabase.child("posts").push().getKey(); Post post = new Post(userId, username, title, body); Map<String, Object> postValues = post.toMap(); Map<String, Object> childUpdates = new HashMap<>(); childUpdates.put("/posts/" + key, postValues); childUpdates.put("/user-posts/" + userId + "/" + key, postValues); mDatabase.updateChildren(childUpdates); }
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 getKey()
. 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 updateChildren()
, 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.
Adicionar um retorno de chamada de conclusão
Se quiser saber quando seus dados foram confirmados, você pode adicionar um ouvinte de conclusão. Tanto setValue()
quanto updateChildren()
utilizam um ouvinte de conclusão opcional que é chamado quando a gravação foi confirmada com sucesso no banco de dados. Se a chamada não tiver êxito, o ouvinte receberá um objeto de erro indicando o motivo da falha.
Kotlin+KTX
database.child("users").child(userId).setValue(user) .addOnSuccessListener { // Write was successful! // ... } .addOnFailureListener { // Write failed // ... }
Java
mDatabase.child("users").child(userId).setValue(user) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Write was successful! // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Write failed // ... } });
Excluir dados
A maneira mais simples de excluir dados é chamar removeValue()
em uma referência ao local desses dados.
Você também pode excluir especificando null
como o valor para outra operação de gravação, como setValue()
ou updateChildren()
. Você pode usar esta técnica com updateChildren()
para excluir vários filhos em uma única chamada de API.
Desanexar ouvintes
Os retornos de chamada são removidos chamando o método removeEventListener()
na referência do banco de dados do Firebase.
Se um ouvinte tiver sido adicionado diversas vezes a um local de dados, ele será chamado diversas vezes para cada evento e você deverá desanexá-lo o mesmo número de vezes para removê-lo completamente.
Chamar removeEventListener()
em um ouvinte pai não remove automaticamente os ouvintes registrados em seus nós filhos; removeEventListener()
também deve ser chamado em qualquer ouvinte filho para remover o retorno de chamada.
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 operação de transação . Você fornece dois argumentos a esta operação: uma função de atualização e um retorno de chamada de conclusão opcional. A função de atualizaçã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:
Kotlin+KTX
private fun onStarClicked(postRef: DatabaseReference) { // ... postRef.runTransaction(object : Transaction.Handler { override fun doTransaction(mutableData: MutableData): Transaction.Result { val p = mutableData.getValue(Post::class.java) ?: return Transaction.success(mutableData) if (p.stars.containsKey(uid)) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1 p.stars.remove(uid) } else { // Star the post and add self to stars p.starCount = p.starCount + 1 p.stars[uid] = true } // Set value and report transaction success mutableData.value = p return Transaction.success(mutableData) } override fun onComplete( databaseError: DatabaseError?, committed: Boolean, currentData: DataSnapshot?, ) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError!!) } }) }
Java
private void onStarClicked(DatabaseReference postRef) { postRef.runTransaction(new Transaction.Handler() { @NonNull @Override public Transaction.Result doTransaction(@NonNull MutableData mutableData) { Post p = mutableData.getValue(Post.class); if (p == null) { return Transaction.success(mutableData); } if (p.stars.containsKey(getUid())) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1; p.stars.remove(getUid()); } else { // Star the post and add self to stars p.starCount = p.starCount + 1; p.stars.put(getUid(), true); } // Set value and report transaction success mutableData.setValue(p); return Transaction.success(mutableData); } @Override public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot currentData) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError); } }); }
Usar uma transação evita que a contagem de estrelas fique incorreta se vários usuários marcarem a mesma postagem com estrela ao mesmo tempo ou se o cliente tiver dados desatualizados. Se a transação for rejeitada, o servidor retorna o valor atual ao cliente, que executa a transação novamente com o valor atualizado. Isso se repete até que a transação seja aceita ou muitas tentativas tenham sido feitas.
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.
Kotlin+KTX
private fun onStarClicked(uid: String, key: String) { val updates: MutableMap<String, Any> = hashMapOf( "posts/$key/stars/$uid" to true, "posts/$key/starCount" to ServerValue.increment(1), "user-posts/$uid/$key/stars/$uid" to true, "user-posts/$uid/$key/starCount" to ServerValue.increment(1), ) database.updateChildren(updates) }
Java
private void onStarClicked(String uid, String key) { Map<String, Object> updates = new HashMap<>(); updates.put("posts/"+key+"/stars/"+uid, true); updates.put("posts/"+key+"/starCount", ServerValue.increment(1)); updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true); updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1)); mDatabase.updateChildren(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 nos quais os ouvintes estão sendo usados ou que estão sinalizados para serem mantidos em sincronia com o servidor. Quando os dados são lidos ou gravados, esta versão local dos dados é usada primeiro. 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 de qualquer interação com o 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
- Trabalhando com listas de dados
- Aprenda como estruturar dados
- Saiba mais sobre recursos on-line e off-line