Ler e gravar dados no Android

Este documento aborda as noções básicas de leitura e gravação de dados do Firebase.

Esses dados são gravados em uma referência FirebaseDatabase 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.

Criar protótipos e fazer testes com o Pacote do emulador local do Firebase (opcional)

Antes de ver informações sobre como um app lê e grava no Realtime Database, confira o Pacote do emulador local 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 do emulador local, 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.

O uso do emulador do Realtime Database envolve apenas algumas etapas:

  1. Para se conectar ao emulador, adicione uma linha de código à configuração de teste do aplicativo.
  2. Execute firebase emulators:start na raiz do diretório do projeto local.
  3. 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 do emulador local.

Receber um DatabaseReference

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

Java

private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

Kotlin+KTX

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

Gravar dados

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:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Transmitir um objeto Java personalizado se a classe que o define tiver um construtor padrão que não recebe argumentos e tem getters públicos para as propriedades a serem atribuídas.

Se você usar um objeto Java, o conteúdo desse objeto será automaticamente associado a locais filhos de maneira aninhada. Usar um objeto Java normalmente torna seu código mais legível e fácil de manter. Por exemplo, se você tiver um app com um perfil básico de usuário, o objeto User poderá ter a seguinte aparência:

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

}

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.
}

É possível adicionar um usuário com setValue() da seguinte maneira:

Java

public void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);

    mDatabase.child("users").child(userId).setValue(user);
}

Kotlin+KTX

fun writeNewUser(userId: String, name: String, email: String) {
    val user = User(name, email)

    database.child("users").child(userId).setValue(user)
}

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:

Java

mDatabase.child("users").child(userId).child("username").setValue(name);

Kotlin+KTX

database.child("users").child(userId).child("username").setValue(name)

Ler dados

Ler dados com listeners permanentes

Para ler dados em um caminho e detectar alterações, use o método addValueEventListener() para adicionar um ValueEventListener à DatabaseReference.

Listener Callback de evento Uso normal
ValueEventListener onDataChange() Ler e detectar alterações em todo o conteúdo de um caminho.

É possível usar o método onDataChange() para ler um snapshot estático do conteúdo de um determinado caminho 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 callback do evento recebe um snapshot que contém todos os dados no local, incluindo dados filhos. Se não houver dados, o snapshot retornará false quando você chamar exists() e null e quando chamar getValue() nele.

O exemplo a seguir mostra um app de blog social recuperando detalhes de uma postagem do banco de dados:

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

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)

O listener 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 houver 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 esse local no banco de dados do Firebase. Este método recebe um objeto DatabaseError indicando por que a falha ocorreu.

Ler dados uma vez

Ler uma vez usando get()

O SDK foi projetado para gerenciar interações com servidores de banco de dados, independentemente do seu app estar on-line ou off-line.

Geralmente, é necessário usar as técnicas de ValueEventListener descritas acima para ler dados e receber notificações sobre as atualizações dos dados do back-end. As técnicas de listener reduzem o uso e o faturamento, além de serem 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, get() não conseguir retornar o valor do servidor, o cliente procurará no cache de armazenamento local e retornará um erro se o valor não for encontrado.

O uso desnecessário de get() pode aumentar a utilização da largura de banda e causar uma perda de desempenho. É possível evitar isso usando um listener em tempo real, como mostrado acima.

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

Kotlin+KTX

mDatabase.child("users").child(userId).get().addOnSuccessListener {
    Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
    Log.e("firebase", "Error getting data", it)
}

Ler uma vez usando um listener

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 addListenerForSingleValueEvent 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 casos anteriores usou esse método para carregar o perfil de um usuário quando ele começou a escrever uma nova postagem.

Atualizar ou excluir dados

Atualizar campos específicos

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

Ao chamar updateChildren(), 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 escalabilidade, atualize todas as instâncias usando a distribuição de dados. Por exemplo, um app de blog social poderia ter uma classe Post como esta:

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

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

Para criar uma postagem e atualizá-la simultaneamente no feed de atividades recentes e no feed de atividades do usuário que fez a postagem, o aplicativo de blog usa códigos como estes:

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

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

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 getKey(). É possível usar a chave 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 updateChildren(), 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 callback de conclusão

Para saber quando seus dados foram confirmados, adicione um listener de conclusão. Tanto setValue() quanto updateChildren() recebem um listener de conclusão opcional, que será chamado quando a gravação for confirmada no banco de dados. Se a chamada não for bem-sucedida, o listener receberá um objeto de erro indicando o motivo da falha.

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

Kotlin+KTX

database.child("users").child(userId).setValue(user)
        .addOnSuccessListener {
            // Write was successful!
            // ...
        }
        .addOnFailureListener {
            // Write failed
            // ...
        }

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 null como o valor de outra operação de gravação, como setValue() ou updateChildren(). Você pode usar essa técnica com updateChildren() para excluir vários filhos em uma única chamada de API.

Remover listeners

Para remover callbacks, chame o método removeEventListener() na sua referência ao banco de dados do Firebase.

Se um listener tiver sido adicionado várias vezes em um local de dados, ele será chamado várias vezes para cada evento, e você precisará desvinculá-lo o mesmo número de vezes para removê-lo por completo.

Chamar removeEventListener() em um listener pai não remove automaticamente os listeners registrados nos nós filhos dele. O removeEventListener() também precisa ser chamado nos listeners filhos para remover o callback.

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 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.

No app de blog social do exemplo, os usuários podem adicionar ou remover estrelas de postagens e acompanhar quantas foram recebidas desta forma:

Java

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @Override
        public Transaction.Result doTransaction(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);
        }
    });
}

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

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. Se a transação for rejeitada, o servidor retornará o valor atual ao cliente, que executará 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 estrelas 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.

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

Kotlin+KTX

private fun onStarClicked(uid: String, key: String) {
    val updates: MutableMap<String, Any> = HashMap()
    updates["posts/$key/stars/$uid"] = true
    updates["posts/$key/starCount"] = ServerValue.increment(1)
    updates["user-posts/$uid/$key/stars/$uid"] = true
    updates["user-posts/$uid/$key/starCount"] = ServerValue.increment(1)
    database.updateChildren(updates)
}

Este código não usa uma operação de transação. Portanto, ele não será executado automaticamente de maneira automática 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 com estrela, 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 uma versão interna própria de quaisquer dados em que os listeners estão sendo usados ou que foram sinalizados para manterem a sincronia com o servidor. Quando os dados são lidos ou gravados, a versão local dos dados é usada primeiro. Depois, o cliente do Firebase sincroniza esses dados com os servidores de banco de dados remotos e com outros clientes de acordo com o modelo “melhor esforço".

Por causa disso, todas as gravações no banco de dados acionarão eventos locais imediatamente antes de qualquer interação com o servidor. Isso significa que o app continuará 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