Aplicação offline usando Flutter e AppSync

Jorge Guilherme
Senior Sistemas
Published in
10 min readJul 6, 2021

A garantia da sincronização de dados de dispositivos móveis desconectados, isto é, sem conexão com alguma rede, é um quesito muito importante no desenvolvimento de aplicações móveis distribuídas e que pode se tornar um problema, caso não seja observado.
Para solucionar este problema existem diversas alternativas, como API HTTP RESTful, WebSocket TCP/IP, API GraphQL entre outros.

A fim de garantir a sincronização de dados com dispositivos offline, podemos utilizar o AppSync, um serviço disponibilizado pela AWS e que tem o intuito de sincronizar dados de aplicações front-ends por meio de uma API GraphQL.
Tal ferramenta possui suporte a várias fontes de dados como o DynamoDB, Bancos de dados relacionais, Lambdas entre outros. Além disso, por utilizar GraphQL, oferece suporte a cache, atualizações em tempo real e escalabilidade para garantir o fluxo de dados em alta demanda e com um ambiente totalmente serverless.

Antes de aprofundarmos mais no AppSync é necessário entender um pouco sobre o conceito GraphQL, que é uma linguagem de consultas para APIs em tempo de execução, fornecendo aos clientes poderes para pedir ou enviar somente o que é realmente necessário para o contexto dos mesmos.

Com o GraphQL nos oferecendo grandes poderes com APIs e o AppSync o ambiente completo para que tudo isso funcione, precisamos entender alguns pontos desta arquitetura, os quais estão representados na Figura 1.

Figura 1 — Arquitetura AppSync.

Na Figura 1(A) são listados alguns clientes que podem se conectar com o AppSync, como por exemplo, mobile e web apps, além de dispositivos IoT. A conexão entre eles é feita por meio de um endpoint de uma API GraphQL.

A Figura 1(B) é toda a estrutura do AppSync, começando pelo schema GraphQL, onde é definido o tipo de dado que será trabalhado nas requisições, bem como modelos, definições de entradas e saídas, consultas, mutações, entre outros.
No intermédio do schema GraphQL, existem os resolvers, que oferecem suporte a mapeamentos nas funções do GraphQL, seja na entrada ou saída dos dados, onde podemos ver na Figura 2, dois fluxogramas de como os resolvers funcionam.

Figura 2 — Resolvers.

Para o primeiro diagrama, mais à esquerda, o resolver apenas é chamado para receber e devolver os dados a API GraphQL, enquanto no segundo diagrama é representado por várias modificações nos dados.
Tais dados podem ser persistidos, através dos data sources, que são as fontes de dados do AppSync, e está representado na Figura 1(B), oferecendo suporte a várias bases de dados, sendo elas: DynamoDB, Amazon Aurora, ElasticSearch, Lambdas, HTTPs e Local (Pub/Sub).
E para integridade e segurança destes dados que serão persistidos nos data sources, o AppSync oferece suporte a autenticação, por meio de chaves de API configuradas diretamente em sua arquitetura.

Com o AppSync desmistificado, iniciou-se a busca por frameworks que fizessem uso desta tecnologia, o que resultou na utilização do AWS Amplify, com o componente DataStorage, responsável por salvar os dados, de maneira offline, e realizar a sincronização com o AppSync assim que uma conexão com a internet estiver disponível, mas em sua versão 0.1.6 para Flutter, a qual na data de publicação deste artigo é a última, está com alguns problemas na sincronia dos dados e foi optado pela implementação através das chamadas nativas ao GraphQL.

A implementação manual de todas as funções do DataStorage do Amplify resultou na arquitetura apresentada na Figura 3.

Figura 3 — Arquitetura da aplidação.

De forma minimalista, o aplicativo funciona inteiramente offline, salvando e recuperando os dados de uma base SQLite.
Quando o dispositivo estiver online, os dados são enviados ao DynamoDB, que é um serviço de banco de dados NoSQL e foi o escolhido como data source do AppSync.

As duas bases, tanto SQLite quanto DynamoDB, possuem uma modelagem similar representada na Figura 4.

Figura 4 — Modelagens.

Um ponto importante sobre esta modelagem é que, devido ao fato de o SQLite não possuir suporte ao tipo BOOLEAN, os campos booleanos são representados como INTEGER nesta arquitetura.

Todo o fluxo do usuário é baseado em dois campos, sendo eles o deleted e o sync, que são responsáveis pelo sincronismo para todas as operações de CRUD, que primeiramente são salvos no SQLite com as devidas flags sinalizadas, e ocorrendo uma mudança no estado da conexão, a sincronia dos dados será acionada.

A data de ocorrência da última sincronização sempre será salva nas preferências dos usuários, que é um banco de dados de chave e valor, também chamado de shared preferences, comumente usado para salvar configurações de aplicações mobiles, o que facilita e reduz o número de dados trafegados em rede, uma vez que não é necessário buscar todos os dados no AppSync, e sim, somente após uma data específica.

Um ponto importante é que esta data, sempre deve ser salva com o GMT zero, assim como todos os campos de data na base de dados para garantir que, mesmo em diferentes fusos horários, os novos dados sempre estarão sincronizados.

De maneira simples, para a resolução de conflitos, foi optado por manter sempre o último dado recebido pelo AppSync, ou seja, se dois ou mais dispositivos atualizarem um mesmo objeto, será considerado o último a persistir os dados.

Antes de iniciar o desenvolvimento e codificação, serão necessárias algumas ferramentas, sendo elas: uma conta na AWS e o SAM, o qual será desenvolvido o template para provisionar todos os recursos necessários para esta aplicação, sem necessidade de criar os recursos manualmente pelo console da AWS.
Todo o código está disponível neste Github.

O primeiro passo para montar esta arquitetura é desenvolver o template YAML, o qual corresponde a uma linguagem de marcação, que é utilizada para descrever e provisionar os recursos utilizados na AWS, o arquivo completo está disponível neste template.

Vale lembrar que este tutorial foi desenvolvido utilizando apenas um template YAML, porém o mesmo pode ser dividido em vários.

Para criação de um template, precisamos definir qual versão do AWSTemplateFormat será utilizado, conforme descrito no Quadro 1.

Quadro 1 — Criação do template.

Um ponto legal do template é que podemos informar parâmetros, assim possibilitando em tempo de build, a passagem de informações. No Quadro 2, existe a a criação de um parâmetro que será utilizado futuramente para nomear o AppSync.

Quadro 2 — Parâmetros.

Com o template iniciado, é necessário a criação dos recursos, o primeiro a ser criado será a tabela no DynamoDB representada no Quadro 3.

Quadro 3 — Criação da tabela DynamoDB.

Linha 2 — Nome do recurso.
Linha 4DeletionPolicy: ação que deve ser feita assim que sua tabela for removida, quando marcada como retain, significa que ao excluir a stack do CloudFormation a tabela continuará existindo.
Linha 8BillingMode: forma de pagamento do Dynamo, que pode ser pago pela utilização “PAY_PER_REQUEST” ou provisionado “PROVISIONED”.
Linha 9AttributeDefinitions: campo utilizado para definição da chave primária do DynamoDB.
Para mais detalhes e atributos do DynamoDB pode-se utilizar a documentação oficial.

A partir de agora, todos os recursos adicionados deverão estar abaixo da propriedade Resources definida no Quadro 3.

Com a tabela já definida é necessário adicionar as políticas de privilégios, para que não seja necessário usar chaves de APIs, quando algum recurso tentar acessar a tabela que criamos anteriormente. A criação desta política esta representada no Quadro 4.

Quadro 4 — Criação de política de privilégios.

Linha 1 — É o nome do recurso.
Linha 4 Description: descrição para identificar esta política futuramente.
Linha 10Actions: será informado o que poderá ser utilizado a quem possuir esta política.
Linha 19Resources: referenciamento de quais tabelas pertence a esta política, onde neste tutorial foi apontado a tabela criada no Quadro 3.

No Quadro 5 é descrita a criação de uma role para que o AppSync consiga obter os privilérios criados anteriormente.

Quadro 5 — Definição de um papel.

Linha 1 — É o nome do recurso.
Linha 3 DependsOn: é a política criada no Quadro 4.
Linha 9 AssumeRolePolicyDocument: é a definição de quais entidades que podem utilizar este papel.

Com a base de dados devidamente implementada, o Quadro 6 apresenta a definição inicial da criação do AppSync.

Quadro 6— Definição do AppSync.

Linha 4 AuthenticationType: é a forma que será feita a autenticação dos endpoints GraphQL, para o exemplo acima foi utilizado a chave de API, mas existem outras formas de autenticação que pode ser consultadas na documentação oficial.
Linha 5 Name: nome para o AppSync, que será exibido no console da AWS, nota-se que neste item utilizamos o parâmetro criado no Quadro 2.

Neste ponto precisamos implementar os esquemas do GraphQL, onde faremos a tipagem, queries e mutations, que está disponível no Quadro 7.

Quadro 7 — Schema GraphQL.

Linha 4ApiId: aqui devemos apontar o AppSync criado anteriormente.
Linha 7Input: é a definição de como os dados devem chegar ao AppSync.
Linha 13Type: é a definição de modelo de dados.
Linha 45Mutation: é o método responsável por efetuar as criações e edições dos dados, a Figura 5 é um exemplo de chamada via API de uma mutation.

Figura 5 — API mutation.

Linha 48Query: é o método responsável por efetuar as consultas dos dados, a Figura 6 é uma exemplo de chamada via API de uma query.

Figura 6 — API qurey.

Com a tipagem dos dados funcionando é hora de informar ao AppSync qual será a fonte dos dados que está exemplificada no Quadro 8.

Quadro 8— Definição do DataSoure.

Linha 4ApiId: aqui devemos apontar o AppSync criado anteriormente.
Linha 5Name: nome dado ao data source, um ponto importante é que pegamos a inicial dos parâmetros do template.
Linha 6Type: informação de qual será o tipo de fonte de dados.
Linha 7ServiceRoleArn: aqui deve ser informado o papel criado no Quadro 5.
Linha 8DynamoDBConfig: deve ser informado neste item a qual tabela este data source pertence.

Os resolvers serão dividos em três partes, a query “listPeople”, a query “getPerson” e o mutation “savePerson”, para os mapeamentos de entrada e saída dos dados é utilizado o apache Velocity Template Language (VTL).

No quadro 9 é a definição da listagem da pessoa, que no esquema do GraphQL foi definida como “listPeople”.

Quadro 9— Difinição do resolver de listagem.

Linha 5 ApiId: aqui devemos apontar o AppSync criado anteriormente.
Linha 6TypeName: tipo da invocação GraphQL.
Linha 7FieldName: nome dado a query no esquema GraphQL.
Linha 8DataSourceName: referência do data source criado no Quadro 8.
Linha 9RequestMappingTemplate: nesta linha é definido o tipo de operação do DynamoDB, definido como “scan”, se irá existir filtros, paginação entre outros. É importante entender que nesta parte podemos efeturar mapeamentos em caso de entradas serem diferentes do que esperado pelo DynamoDB.
Linha 19ResponseMappingTemplate: refere-se ao mapeamento de saída, ou seja, é possível transformar os dados da fonte para uma saída esperada pelo GraphQL.

No quadro 10 é a definição da busca de pessoa, que no esquema do GraphQL foi definida como “getPerson”.

Quadro 8 — Definição de busca por ID.

O Quadro 11 é a definição da persistência dos dados da pessoa, que no esquema do GraphQL foi definida como “savePerson”.

Quadro 9 — Definição da persistencia de dados.

Para finalizar o back-end é possível adicionar uma camada de segurança nas chamadas ao AppSync utilizando uma API_KEY que pode ser vista no Quadro 12.

Caso esta camada de segurança seja adicionada, todas as chamadas devem possuir o Header x-api-key.

Quadro 12— Definição API_KEY.

Com o template finalizado é necessário enviar um comando de build para o SAM, para que toda a stack seja criada na AWS, para isso utilize o comando abaixo.

sam deploy --guided --capabilities CAPABILITY_NAMED_IAM -t templates/template.yml

O template aqui criado irá gerar uma stack no CloudFormation da AWS, e pode-se adicionar outputs no template para rápida obtenção do Endpoint GraphQL, API_Key e DynamoDB disponível no Quadro 13, porém também é possível obter manualmente via console AWS.

Quadro 13 — Outputs do template.

Quanto ao Flutter, a tecnologia para aplicações multi-plataforma escolhida como padrão pelo time de pesquisa e arquitetura da Senior Sistemas, definida através deste post, contém duas classes principais para a integração deste fluxo.
A primeira classe, cujo código pode ser observado no Quadro 14, é responsável pelo sincronismo dos dados.

Quadro 14 — Sincronia dos dados

Linha 4 — Contém um exemplo da mudança de estado na internet, que se for diferente de none irá chamar a integração.
Linha 11 — Método responsável por executar os envios ao AppSync.

A segunda classe base, é a implementação do SQLite, onde são persistidos todos os dados locais, que está representada no Quadro 15.

Quadro 15 — Métodos do SQLite.

Um ponto importante nesta classe de persistências dos dados, é que todos os métodos chamam a sincronia, para tentativa de enviar ao AppSync instantaneamente, e assim toda a aplicação Flutter conhece apenas a classe do SQLite, o que é algo parecido com o que o Amplify faz por meio do DataStorage.

Todo o código Flutter utilizado neste post está disponível neste Github.

Mesmo que o AppSync seja um serviço relativamente recente, mostrou-se bastante eficaz e estável na construção de APIs, sem precisar construir backend de fato, bastando simples marcações para que a aplicação Flutter tivesse suporte a um ambiente cloud totalmente serverless, que juntamente com estratégia de arquitetura offline, gerou uma percepção para o usuário de um sistema que sempre está disponível, mesmo operando fora de uma conexão com a internet.

A Figura 6 é um exemplo da arquitetura aqui implementada funcionando.

Figura 6 — Funcionamento da arquitetura.

Existe um ponto ainda em aberto para esta arquitetura, pois não foi definida uma estratégia de exclusão física de dados no DynamoDB, e que poderia ser resolvido por meio de um TTL (Tempo de vida), para que a exclusão física seja realizada após um determinado tempo da exclusão lógica.

Apesar de existir um ponto de melhoria, a arquitetura conseguiu resolver de maneira fácil e objetiva problemas de conexões com a internet, sincronia em diferentes GMTs, configurações extensas de ambiente e abstraiu desenvolvimentos de recursos já implementados por meio do AppSync juntamente com o DynamoDB, que se mostraram uma ótima alternativa quando o assunto é funcionamento e sincronia offline.

--

--