Ir para o console

Ler e gravar dados no Android

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 uma referência FirebaseDatabase e recuperados por meio de um listener assíncrono anexado à referência. O listener é acionado uma vez para o estado inicial dos dados e novamente quando os dados são alterados.

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

private lateinit var database: DatabaseReference// ...
database = FirebaseDatabase.getInstance().reference

Ler e 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 substitua os dados existentes no caminho. Use esse método para:

  • passar os tipos que correspondam aos tipos JSON disponíveis:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • passar 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 mapeado para 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

@IgnoreExtraProperties
data class User(
    var username: String? = "",
    var email: String? = ""
)

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

Java

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

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

Kotlin

private 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

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

Detectar eventos de valor

Para ler dados em um caminho e detectar as alterações, use os métodos addValueEventListener() ou addListenerForSingleValueEvent() para adicionar um ValueEventListener ao DatabaseReference.

Listener Retorno de chamada de evento Uso típico
ValueEventListener onDataChange() Ler e detectar alterações em todo o conteúdo de um caminho.

É possível usar o evento onDataChange() para ler um snapshot estático do conteúdo de um determinado caminho, já que ele existia 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 retorno de chamada 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

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::class.java)
        // ...
    }

    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 é chamado se a leitura for cancelada. Por exemplo, uma leitura pode ser cancelada se o cliente não tiver permissão de leitura desse local no banco de dados do Firebase. Este método recebe um objeto DatabaseError indicando por que a falha ocorreu.

Ler dados uma vez

Em alguns casos, é necessário executar um retorno de chamada apenas uma vez e, depois, removê-lo imediatamente. Por exemplo, quando inicializar um elemento de IU que não precisa ser alterado. É possível usar o método addListenerForSingleValueEvent() para simplificar esse cenário: ele é acionado somente uma vez.

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:

Atualização ou exclusão de 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

@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

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 = HashMap<String, Any>()
    childUpdates["/posts/$key"] = postValues
    childUpdates["/user-posts/$userId/$key"] = 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 retorno de chamada de conclusão

Para saber quando seus dados foram confirmados, é possível adicionar um listener de conclusão. Ambos setValue() e updateChildren() recebem um retorno de 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

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 onde estão localizados.

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(). Use essa técnica com updateChildren() para excluir vários filhos em uma única chamada de API.

Remover listeners

Para remover retornos de chamada, 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á desanexá-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 também precisa ser chamado nos listeners filhos para remover o retorno de chamada.

Salvar dados como transações

Para tratar dados 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 retorno de chamada 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 b,
                               DataSnapshot dataSnapshot) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError);
        }
    });
}

Kotlin

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?,
            b: Boolean,
            dataSnapshot: 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.

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

Quando a conectividade for restabelecida, seu app recebe o conjunto apropriado de eventos para que o cliente seja sincronizado com o estado atual do servidor, sem necessidade de criar um código personalizado.

A seguir