CRUD (Create, Read, Update, Delete) com Firebase Cloud Firestore no Flutter

Série: Dominando o Firebase em Aplicativos Flutter

CRUD (Create, Read, Update, Delete) com Firebase Cloud Firestore no Flutter

Fala, devs. Blz? Hoje vamos dar continuidade a série "Dominando o Firebase em Aplicativos Flutter" uma sequência de artigos sobre as principais funcionalidades do Firebase e como integrá-las ao seu aplicativo Flutter.💙

Seja para autenticação de usuários, armazenamento de dados, envio de notificações push ou captura e análise de erros, seus produtos sempre me ofereceram a solução perfeita.

Segundo o blog oficial do Firebase "Mais de 3 milhões de desenvolvedores, de startups a empresas, usam o Firebase para alcançar e envolver bilhões de usuários"

Hoje meu objetivo é abordar o uso do Cloud Firebase um banco de dados NoSQL flexível e escalonável, criado com a infraestrutura do Google Cloud, para armazenar e sincronizar dados no desenvolvimento do cliente e do servidor.

Construiremos um aplicativo de lista de tarefas de exemplo usando o Flutter e Firebase onde executaremos algumas operações com dados.

Vamos usar o design criado por Diaa Mohamed disponível no Dribbble para nos inspirar na construção do nosso app usando firebase.

Então vem comigo. 🤏🏻 👨🏻‍💻

O que é um Crud?

CRUD é um acrônimo que representa as quatro operações básicas usadas em sistemas de gerenciamento de banco de dados relacionais ou em serviços de API que interagem com recursos. As operações são:

  1. Create (Criar): Envolve a criação de novos registros ou recursos no banco de dados ou sistema.

  2. Read (Ler): Envolve a recuperação de dados existentes do banco de dados ou sistema.

  3. Update (Atualizar): Envolve a modificação dos dados existentes no banco de dados ou sistema.

  4. Delete (Excluir): Envolve a remoção de registros ou recursos existentes do banco de dados ou sistema.

Essas operações são essenciais em muitas aplicações de software para realizar operações básicas de manipulação de dados. Quando se fala em desenvolvimento web, por exemplo, CRUD é uma abordagem comum para a implementação de funcionalidades de back-end que permitem aos usuários criar, ler, atualizar e excluir dados em um sistema.

No contexto do Firebase, um CRUD refere-se às operações básicas que podem ser realizadas em seu banco de dados em tempo real, que geralmente é o Firebase Realtime Database ou o Firestore 🔥.

O que é Cloud Firestore?

O Cloud Firestore é um banco de dados flexível e escalonável para desenvolvimento focado em dispositivos móveis, Web e servidores pelo Firebase e do Google Cloud.

Ele mantém seus dados sincronizados entre aplicativos clientes por meio de ouvintes em tempo real e oferece suporte off-line para que você possa criar aplicativos responsivos que funcionam independentemente da latência da rede ou da conectividade com a Internet.

Alguns dos principais recursos são:

Sincronização em tempo real: O Firestore oferece atualizações em tempo real para os clientes conectados. Isso significa que os dados são sincronizados automaticamente entre os diferentes dispositivos e plataformas em tempo real, permitindo uma experiência de usuário fluida e em tempo real.

Consultas ricas: O Firestore suporta uma variedade de consultas, incluindo consultas simples, consultas compostas e ordenação. Isso permite que os desenvolvedores recuperem dados de maneira eficiente e flexível.

Estrutura de dados escalável: O Firestore organiza os dados em coleções e documentos. Ele permite uma estrutura hierárquica flexível, o que facilita a organização e a consulta de dados.

Escalabilidade automática: O Firestore é altamente escalável e gerenciado pelo Google Cloud Platform. Ele escala automaticamente para atender às demandas de tráfego e armazenamento, garantindo alta disponibilidade e desempenho.

Suporte off-line: Armazena em cache os dados ativamente usados pelo aplicativo. Dessa maneira, o aplicativo poderá escrever, ler, detectar e consultar dados, mesmo que o dispositivo esteja desconectado. Quando o dispositivo retorna ao estado on-line, as alterações locais são sincronizadas novamente no Cloud Firestore.

Se você quiser se aprofundar um pouco mais no funcionamento deixo essa playlist muito completa sobre o assunto.

Modelo de dados do Cloud Firestore

O Cloud Firestore utiliza um modelo de dados NoSQL orientado a documentos, o que significa que ele difere de um banco de dados relacional SQL. Ao invés de tabelas e linhas, você armazena dados em documentos que posteriormente são organizados em coleções.

Coleções contem vários documentos

Veja alguns pontos-chave sobre o modelo de dados do Firestore:

  • Documentos: Cada documento age como uma unidade de dados auto-contida, similar a uma linha em uma tabela relacional. Documentos contêm pares chave-valor, onde a chave é uma string que identifica o dado e o valor pode ser de diversos tipos como strings, números, booleanos, objetos, arrays, referências a outros documentos, entre outros.

    Exemplo de documento

  • Coleções: Documentos relacionados logicamente são agrupados em coleções. Pense em coleções como pastas que organizam seus documentos. Por exemplo, você pode ter uma coleção "usuarios" para armazenar documentos de usuários do sistema, e cada documento representaria um usuário individual

  • Sem Esquema: Firestore é schemaless, ou seja, não possui um esquema rígido que define a estrutura dos dados. Isso oferece flexibilidade, pois você pode adicionar novos campos a documentos existentes sem precisar modificar toda a coleção.

  • Hierarquia: Documentos podem conter subcoleções, permitindo uma modelagem hierárquica dos seus dados. Por exemplo, a coleção "usuarios" poderia conter uma subcoleção "pedidos" para armazenar os pedidos realizados por cada usuário.

Cloud Firestore X Realtime Database: qual devo usar?

O Realtime Database é um banco de dados que usa documentos JSON para armazenar os dados em pares de chave-valor enquanto o Cloud Firestore é uma versão mais recente do banco de dados que usa coleções de documentos para armazenar dados.

Existe algumas diferenças entre eles e abaixo vou citar 2 dos principais.

  • Modelo de Dados:

    • Realtime Database: Utiliza um modelo de dados JSON em tempo real. Isso significa que os dados são sincronizados em tempo real entre todos os clientes que estão conectados ao banco de dados.

    • Cloud Firestore: Usa um modelo de dados de documentos coleções. Isso permite uma estrutura hierárquica mais flexível para dados, onde os documentos podem conter coleções aninhadas.

  • Preço:

    • Realtime Database: O modelo de preços é baseado na largura de banda e no espaço de armazenamento utilizados.

    • Cloud Firestore: O modelo de preços é baseado na quantidade de leituras, gravações, exclusões e largura de banda utilizadas. Nesse caso um ponto de atenção ao modelar sua base 🚨🚨.

OBS. Qual usar depende muito das necessidades específicas do seu projeto:

  • Realtime Database é uma boa escolha se você precisa de sincronização em tempo real de dados simples e tem requisitos de escalabilidade moderados.

  • Cloud Firestore é preferível para aplicativos que requerem uma estrutura de dados mais complexa, consultas flexíveis e alta escalabilidade.

Em resumo, se você precisa de uma estrutura de dados mais flexível, consultas poderosas e escalabilidade robusta, o Cloud Firestore é provavelmente a melhor opção. Se você precisa de uma sincronização em tempo real simples e não tem requisitos de consulta muito complexos, o Realtime Database pode ser mais adequado.

Se você quiser se aprofundar um pouco mais nesse assunto acesse os links abaixo.

  1. https://firebase.google.com/docs/firestore/rtdb-vs-firestore?hl=pt-br

  2. https://stackoverflow.com/questions/46549766/whats-the-difference-between-cloud-firestore-and-the-firebase-realtime-database

Ativando o Firestore

Vamos utilizar o projeto flutter-firebase-app que criamos e configuramos no artigo passado, se você é novo acesse aqui.

1 - Vá até o console do Firebase e clique no seu projeto. No menu Criação , clique em Firestore Database > Criar banco de dados.

Ativação do cloud firestore

2 - A próxima tela aparecerá solicitando que você selecione o local que será hospedado o banco de dados. Você pode usar o padrão mas fique certo que não poderá alterar o local posteriormente.
Selecionar o local do banco de dados do Firestore durante a ativação é crucial por várias razões como:

  1. Latência: Escolher um local próximo aos usuários finais reduz a latência e melhora a experiência do usuário.

  2. Conformidade: Pode ser necessário garantir que os dados estejam armazenados em uma região específica devido a requisitos legais ou regulatórios.

  3. Resiliência: Distribuir os dados em vários locais aumenta a resiliência da aplicação, garantindo disponibilidade mesmo em caso de falhas em um data center.

  4. Custo: Escolher o local mais econômico pode reduzir os custos operacionais, já que alguns provedores de nuvem podem cobrar taxas diferentes com base na localização dos servidores.

3 - Selecione Iniciar em modo de teste e leia a isenção de responsabilidade sobre as regras de segurança. O modo de teste garante que você possa gravar livremente no banco de dados durante o desenvolvimento. Em seguida, clique no botão Criar para prosseguir.

🚨 Para seus aplicativos, especialmente aplicativos de produção, é importante proteger seu banco de dados com regras de segurança. Para saber mais sobre regras de segurança, consulte Regras de segurança do Firebase .

Fazendo o CRUD no Firestore

Adicione o package do cloud firestore no seu projeto

dependencies:
  cloud_firestore: ^4.15.10

No Firestore teremos uma coleção chamada todos que seria usada para armazenar dados relacionados a cada um dos itens de tarefas que os usuários podem adicionar, editar ou remover.

Cada item consistirá nos seguintes campos (os tipos de dados são escritos entre colchetes):

  • title (String): armazena o nome da tarefa pendente

  • createdAt (Timestamp): data de criação da pendência

  • completed (Booleano): se a tarefa pendente foi concluída

  • id(String): identificador exclusivo de uma tarefa

No Dart, vamos desenvolver um objeto para manipular esses dados e facilitar a integração com o Firestore. No entanto, os dados são intercambiados com o Firestore no formato JSON. Portanto, nosso modelo deve incluir uma funcionalidade para converter os dados de e para um objeto Dart.

Para essa conversão, podemos implementar um método toMap() que será responsável por retornar um Map quando dado um objeto Dart, e também podemos utilizar um construtor de fábrica fromJson para facilitar a conversão de JSON para objetos Dart. Aqui está um exemplo de como isso pode ser feito:

Com esse código, você pode facilmente criar objetos Todo a partir de dados JSON usando o construtor fromJson, e também pode converter objetos Todo em um formato que pode ser armazenado ou transmitido como JSON usando o método toMap().

import 'package:cloud_firestore/cloud_firestore.dart';

class TodoModel {
  String? id;
  final String title;
  final bool completed;
  final DateTime createdAt;

  TodoModel({
    this.id,
    required this.title,
    this.completed = false,
    required this.createdAt,
  });

  // Construtor Factory para converter JSON em objeto Dart
  factory TodoModel.fromJson(Map<String, dynamic> json) {
    return TodoModel(
      id: json['id'],
      title: json['title'],
      completed: json['completed'],
      createdAt: DateTime.fromMicrosecondsSinceEpoch(
        (json['createdAt'] as Timestamp).microsecondsSinceEpoch,
      ),
    );
  }
  // Método para converter objeto Dart em um Map
  Map<String, dynamic> toMap() {
    return {
      'title': title,
      'completed': completed,
      'createdAt': createdAt,
    };
  }

  TodoModel copyWith({
    String? id,
    String? title,
    bool? completed,
    DateTime? createdAt,
  }) {
    return TodoModel(
      id: id ?? this.id,
      title: title ?? this.title,
      completed: completed ?? this.completed,
      createdAt: createdAt ?? this.createdAt,
    );
  }
}

Gravando dados

  • Create (Criar): No Firebase, você pode criar novos registros no banco de dados usando uma operação de escrita, como .set() ou .add() dependendo do tipo de banco de dados que você está usando.

    Ao usar set() para criar um documento, você precisa especificar um ID para ele. Exemplo:

      db.collection("cities").doc("new-city-id").set({"name": "Chicago"});
    

    No entanto, às vezes não há um ID significativo para o documento. É mais prático que o Cloud Firestore gere um automaticamente para você. Para fazer isso, podemos usar o add() na hora de criar o novo registro. Em nosso aplicativo de exemplo vamos utilizar-lo.

    Adicionar uma tarefa (todo) (CRIAR):

    O método addTodo é definido para adicionar uma nova tarefa à coleção ‘todos’ no Firestore. Veja o exemplo abaixo.

  Future<String> addTodo(TodoModel model) async {
     try {
          var snapshot = await firestore.collection('todos').add(model.toMap());
          return snapshot.id;
        } on FirebaseException catch (e) {
          debugPrint("Failed with error '${e.code}': ${e.message}");
          throw Exception(e.message);
        }
   }

  • Read (Ler): Você pode ler dados do banco de dados usando operações de leitura, como .get() para Futures ou .snapshots() para Streams, que permitem recuperar dados de um caminho específico no banco de dados em tempo real.

    Obtenha tarefas (LER):

    O método getTodos é implementado para recuperar a lista de dados da coleção de 'todos'.

    •     Future<List<TodoModel>> getTodos() async {
              try {
                var snapshot = firestore.collection('todos').get();
                return snapshot.then(
                  (querySnapshot) {
                    return querySnapshot.docs.map((todo) {
                      var mapData = todo.data();
                      mapData['id'] = todo.id;
                      return TodoModel.fromJson(mapData);
                    }).toList();
                  },
                );
              } on FirebaseException catch (e) {
                debugPrint("Failed with error '${e.code}': ${e.message}");
                throw Exception(e.message);
              }
            }
      

  • Update (Atualizar): Para atualizar dados existentes no Firebase, você pode usar a função .update() para fazer alterações em campos específicos de um registro, ou a função .set() com a opção merge: true para mesclar dados novos e existentes.

    Para atualizar alguns campos de um documento sem substituir o documento inteiro, o update() é o ideial pois em nosso app atualizaremos o campo completed quando finalizarmos uma task em nosso aplicativo.

    É possível configurar um campo no documento como um carimbo de data/hora do servidor que detecta quando o servidor recebe a atualização basta usar o FieldValue.serverTimestamp(). Veja o exemplo abaixo.

    Atualização de um todo (ATUALIZAÇÃO):

    O método updateTodo é definido para atualizar um todo existente na coleção 'todos'. O método *.*doc(docId) : Obtém uma referência a um documento específico usando seu ID.

      Future<void> updateTodo(String id) async {
          try {
            var snapshot = firestore.collection('todos').doc(id);
            await snapshot.update({
              'completed': true,
              'updateAt': FieldValue.serverTimestamp(),
            });
          } on FirebaseException catch (e) {
            debugPrint("Failed with error '${e.code}': ${e.message}");
            throw Exception(e.message);
          }
        }
    

  • Delete (Excluir): Para excluir dados do Firebase, você pode usar a função .delete() para remover um registro específico do banco de dados.

    Excluir todo (DELETE):

    O método deleteTodo é implementado para excluir um registro da coleção ‘todos’.

  •          Future<void> deleteTodo(String id) async {
                try {
                  final snapshot = firestore.collection('todos').doc(id);
                  return await snapshot.delete();
                } on FirebaseException catch (e) {
                  debugPrint("Failed with error '${e.code}': ${e.message}");
                  throw Exception(e.message);
                }
              }
    

Essas operações CRUD são fundamentais para interagir com o banco de dados do Firestore e são usadas para criar, recuperar, atualizar e excluir dados em aplicativos desenvolvidos com o Firebase.

No nosso app de exemplo criamos uma classe chamada FirestoreService que fornece uma forma estruturada e modular de interagir com o banco de dados Firestore, implementando todas as operações necessárias.

Para mais informações sobre gerenciar dados no Firestore clique aqui.

Excluir coleções?

O Firebase não nos dá um método diretamente para excluir toda uma coleção de dados. Uma forma que temos de excluir uma coleção ou subcoleção completa no banco, seria recuperar (leia) todos os documentos dentro da coleção ou subcoleção e os excluir. Abaixo deixo um exemplo de como fazer isso 😎.

OBS: Esse processo gera custos de leitura e exclusão.

 Future<void> deleteAllData() async {
    try {
      final snapshot = await firestore.collection('todos').get();
      final List<DocumentSnapshot> docs = snapshot.docs;
      for (DocumentSnapshot doc in docs) {
        await doc.reference.delete();
      }
    } on FirebaseException catch (e) {
      debugPrint("Failed with error '${e.code}': ${e.message}");
      throw Exception(e.message);
    }
  }

Para explorar o código completo do aplicativo de exemplo e se aprofundar nos detalhes da implementação, você pode encontrar o projeto completo no GitHub. Fique à vontade para acessar o repositório através do seguinte link:

Minha recomendação é que você baixe o projeto e dê uma olhada em toda a implementação pois além de Firebase, usei coisas como provider e change notifier para termos um aplicativo mais completo.

Conclusão

Bom é isso 😎.

Neste artigo, você aprendeu como utilizar o Firestore em um aplicativo real realizando operações na base de dados.

Em artigos futuros da série, veremos como usar outros recursos do Firebase como: Authentication, Crashlytics, Remote Config e muito mais com Flutter.

Espero que você tenha gostado e obrigado por acompanhar até aqui! Compartilhe-o com seus amigos e colegas!

Juntos, vamos construir apps incríveis que transformam o mundo!

Se tiver alguma dúvida ou contribuição, deixe nos comentários!

Me siga para estar sempre por dentro dos próximos artigos 📲 🚀

🌐 Minhas redes sociais 🌐

GitHub | LinkedIn | Instagram | Twitter | Medium