Leitura de dados em tempo real em Flutter com banco de dados relacional ou Firestore com segurança

3 possibilidades de arquiteturas com banco de dados relacional e Firestore que permitem a criação de aplicativos em Flutter em tempo real com segurança
04 de Abril de 2021

Flutter permite criar widgets que são atualizados em tempo real, através da criação de StatefulWidget ou com o uso de builders, tal como StreamBuilder e FutureBuilder. É fácil criar real-time apps usando Flutter no lado do cliente.

Porém, quanto ao lado do servidor? Como implementar um servidor que envie dados para o cliente em Flutter em tempo real?

Firestore e a segurança

Firebase oferece um cliente em Flutter que permite atualizar os Widgets em tempo real. O cliente recebe um novo dado logo após que o respectivo dado foi modificado, porém, algumas vulnerabilidades podem ocorrer quando o cliente se comunica diretamente com o banco de dados.

Firestore Security Rules permite definir um escopo que limita a leitura e modificação de dados dependendo do usuário que está solicitando realizar a leitura ou escrita, mas como criar um mecanismo de segurança que garanta que toda vez que um dado for alterado, outro dado seja alterado também? Imagine o seguinte exemplo de um banco digital em que existem apenas 2 usuários: A e B. Ambos com $100 de saldo na conta. Supondo que o Usuário A deseja enviar $15 para o Usuário B, a transferência de dinheiro consiste em 2 operações principais no banco de dados:

1ª: Usuário A tem seu saldo diminuído: $100 - $15 = $85

2ª: Usuário B tem seu saldo aumentado: $100 + $15 = $115

As operações ou devem ser ambas executadas completamente, ou nenhuma operação deve ser executada. A execução da 1ª operação sem a execução da 2ª operação irá fazer com que dinheiro desapareça, logo, gera um problema na integridade de dados. Para isso, bancos de dados permitem o uso de transactions, estas oferecem atomicidade ao garantir que um conjunto de modificações no banco sejam executadas juntas, mas caso houver algum erro, esse conjunto de operações é cancelado por completo e o banco de dados permanece como se nada houvesse acontecido.

Vamos analisar as possibilidades que temos:

Opção 1: Transactions no lado do cliente

Firestore permite a criação de transactions, mas elas por si só não garantem segurança, pois o cliente pode executar apenas uma parte da operação, para isso é necessário definir regras de segurança para transactions.

A função getAfter auxilia na criação dessa regra, ela retorna um document supondo que a transaction fosse completada.

Ao criar sua regra corretamente usando getAfter, caso o cliente execute apenas uma parte da operação, toda a operação será cancelada e nada irá mudar no seu banco de dados.

Opção 2: Flutter com API REST e Firestore

Uma possibilidade é a criação de uma API REST que seja responsável por executar operações de escrita no banco de dados do Firebase com o uso de transactions. O cliente (Flutter) tem apenas permissões de realizar leitura diretamente no banco de dados do Firestore, assim, o cliente pode ouvir por modificações dos dados em tempo real, mas quando o cliente desejar modificar dados, ele deve realizar requisições HTTP dos tipos POST, PUT ou DELETE na API REST.

arquitetura aplicativo flutter cliente comunica servidor api rest qualquer banco de dados sql firestore

Arquitetura em que o cliente se comunica com a API REST e banco de dados Firestore

Regras de leitura e escrita podem ser configuradas através do Firestore Security Rules, assim o cliente pode realizar operações de leitura, mas não de escrita no banco de dados. Regras de leitura adicionais precisam ser configuradas para evitar que um usuário leia dados que não deve.

Algumas considerações adicionais:

  • Ainda assim, é possível permitir em algumas regras do Firebase Security Rules que o cliente realize algumas operações de escrita diretamente no banco de dados Firestore quando estas operações não oferecem riscos de gerar problemas de integridade dos dados, como, por exemplo, uma simples mudança na data de nascimento.

  • A API REST poderia ser substituída por uma Cloud Function, mas atenção, functions são stateless, ou seja, as variáveis criadas em tempo de execução desaparecerão após que a execução de uma cloud function for finalizada, por exemplo, não é possível manter uma variável do tipo inteiro armazenando a quantidade de vezes que uma cloud function foi chamada.

Outro detalhe importante é a necessidade de organizar as regras e collections no Firestore de modo que o usuário possa ter acesso completo ao document que ele vai realizar operações de leitura, pois o Firestore Security Rules não permite que apenas alguns campos de um document sejam lidos por um usuário e outros não, ou seja, um usuário que precisa ler alguns campos de um document tem permissão de ler ele por completo, por isso é importante você organizar as collections do seu banco do Firestore de maneira que se torne possível configurar as regras apropriadamente.

Mas caso você queira utilizar um outro banco de dados, como um banco de dados SQL por exemplo, como proceder? Para isso, a opção 3 ou 4 se demonstra a mais apropriada.

Opção 3: Flutter com implementação de um servidor com o protocolo websocket

Um servidor pode ser implementado utilizando o protocolo websocket (ao invés de http) e assim, Flutter se comunica com o servidor e atualiza os seus widgets com o uso de StreamBuilder, porém, uma série de tratamentos são necessários ao implementar esse servidor do zero, pensando em facilitar isso é que surgiu o framework Askless.

Opção 4: Flutter com um servidor implementado com o framework Askless

Askless é um framework que facilita o desenvolvimento de servidores para clientes em Flutter e JavaScript, permitindo que estes clientes se comportem em tempo real independentemente do banco de dados que estão utilizando, este framework trata os principais problemas que surgeriam ao implementar manualmente um servidor websocket do zero e oferece suporte ao lado cliente em Flutter para, dentre outras coisas, facilitar a criação de widgets que exibirão os dados obtidos do servidor na tela do aplicativo em Flutter. Como dito no GitHub, Askless permite:

  • 🤝 realizar uma conexão websocket para troca de dados que:

    • 📳 suporta integração com streams no cliente em Flutter

    • 💻 suporta clientes JavaScript: Web e Node.js

    • ↪️ realiza o reenvio de dados em caso de instabilidade da conexão do cliente

    • 🏷️ trata múltiplas e idênticas requisições de listen vindas do mesmo cliente como uma só pelo servidor

  • ✏️ criar as próprias operações CRUD com qualquer banco de dados que você preferir (Create, Read, Update e Delete)

  • restringir o acesso do cliente com as operações CRUD

  • 📣 notificar em tempo real clientes que estão ouvindo por mudanças de uma rota, podendo ser:

    • 🚷 apenas clientes específicos irão receber os dados

    • ✔️ todos os clientes irão receber os dados

  • 🔒 aceitar e recusar tentativas de conexão

Como Askless age como um intermediador da comunicação entre o cliente e o banco de dados, o cliente em Flutter pode realizar operações de escrita e leitura em tempo real no banco de dados, seja relacional ou NoSQL, para que isso aconteça você precisará desevolver o servidor para enviar as atualizações para o cliente em Flutter e/ou JavaScript.

arquitura aplicativo flutter cliente comunica apenas servidor askless qualquer banco de dados firestore sql

Arquitetura em que o cliente em Flutter se comunica apenas com o servidor Askless

Conclusão

Nesse post, foi feito uma breve explanação sobre 3 possibilidades de arquiteturas que permitem construir facilmente real-time apps em Flutter com bancos de dados relacionais e NoSQL sem abrir mão da segurança.