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

  1. Para se conectar ao emulador, adicione uma linha de código à configuração de teste do app.
  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 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.

Receber um DatabaseReference

Para ler ou gravar dados no banco de dados, é necessário 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

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>
  • Transmita 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, é possível que o objeto User tenha a aparência a seguir:

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

}

É possível adicionar um usuário com setValue() como mostrado a seguir:

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

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 de usuário deles como mostrado a seguir:

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 listeners permanentes

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

Listener Callback do 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 aplicativo de blog social recuperando 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 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.

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

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 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 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 em 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 a mostrada a seguir:

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

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

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(). 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 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 vai 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 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 a um local de dados, ele será chamado diversas vezes para cada evento e precisará ser removido o mesmo número de vezes para ser completamente excluído.

Chamar removeEventListener() em um listener pai não remove automaticamente os listeners registrados nos nós filhos dele. removeEventListener() também precisa ser chamado nos listeners filhos para remover a 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.

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:

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

A transação impede uma contagem incorreta de estrelas caso vários usuários adicionem simultaneamente estrelas à mesma postagem ou se o cliente tiver 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 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.

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

Assim que a conectividade for restabelecida, seu app receberá o conjunto apropriado de eventos para que o cliente faça 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