Resolvendo problemas comuns entre plataformas ao trabalhar com Flutter

Publicados: 2022-03-10
Resumo rápido ↬ Ao usar estruturas de plataforma cruzada, as pessoas podem esquecer as nuances de cada uma das plataformas nas quais desejam que seu código seja executado. Este artigo tem como objetivo abordar isso.

Já vi muita confusão online em relação ao desenvolvimento da Web com o Flutter e, muitas vezes, é pelos motivos errados.

Especificamente, as pessoas às vezes o confundem com os antigos frameworks multiplataforma móveis (e desktop) baseados na Web, que basicamente eram apenas páginas da Web executadas em navegadores executados em um aplicativo wrapper.

Isso era realmente multiplataforma no sentido de que as interfaces eram as mesmas de qualquer maneira, porque você só tinha acesso às interfaces normalmente acessíveis na Web.

O Flutter não é isso: ele roda nativamente em cada plataforma, e isso significa que cada aplicativo roda exatamente como se fosse escrito em Java/Kotlin ou Objective-C/Swift no Android e iOS, praticamente. Você precisa saber disso porque isso implica que você precisa cuidar das muitas diferenças entre essas plataformas tão diversas.

Neste artigo, veremos algumas dessas diferenças e como superá-las. Mais especificamente, falaremos sobre as diferenças de armazenamento e interface do usuário, que são as que mais causam confusão aos desenvolvedores ao escrever código Flutter que eles desejam que seja multiplataforma.

Mais depois do salto! Continue lendo abaixo ↓

Exemplo 1: Armazenamento

Recentemente, escrevi em meu blog sobre a necessidade de uma abordagem diferente para armazenar JWTs em aplicativos da Web em comparação com aplicativos móveis.

Isso se deve à natureza diferente das opções de armazenamento das plataformas e à necessidade de conhecer cada uma e suas ferramentas nativas de desenvolvimento.

Rede

Quando você escreve um aplicativo Web, as opções de armazenamento que você tem são:

  1. download/upload de arquivos para/do disco, que requer interação do usuário e, portanto, é adequado apenas para arquivos destinados a serem lidos ou criados pelo usuário;
  2. usar cookies, que podem ou não ser acessíveis a partir de JS (dependendo se são ou não httpOnly ) e são enviados automaticamente junto com as solicitações para um determinado domínio e salvos quando chegam como parte de uma resposta;
  3. usando JS localStorage e sessionStorage , acessíveis por qualquer JS no site, mas apenas a partir de JS que faça parte das páginas desse site.

Móvel

A situação quando se trata de aplicativos móveis é completamente diferente. As opções de armazenamento são as seguintes:

  1. documentos de aplicativos locais ou armazenamento em cache, acessíveis por esse aplicativo;
  2. outros caminhos de armazenamento local para arquivos criados pelo usuário/legíveis;
  3. NSUserDefaults e SharedPreferences respectivamente no iOS e Android para armazenamento de valor-chave;
  4. Keychain no iOS e KeyStore no Android para armazenamento seguro de, respectivamente, quaisquer dados e chaves criptográficas.

Se você não souber disso, fará uma bagunça em suas implementações porque precisa saber qual solução de armazenamento está realmente usando e quais são as vantagens e desvantagens.

Soluções multiplataforma: uma abordagem inicial

O uso do pacote shared_preferences do Flutter usa localStorage na Web, SharedPreferences no Android e NSUserDefaults no iOS. Isso tem implicações completamente diferentes para seu aplicativo, especialmente se você estiver armazenando informações confidenciais, como tokens de sessão: localStorage pode ser lido pelo cliente, portanto, é um problema se você estiver vulnerável a XSS. Embora os aplicativos móveis não sejam realmente vulneráveis ​​ao XSS, SharedPreferences e NSUserDefaults não são métodos de armazenamento seguros porque podem ser comprometidos no lado do cliente, pois não são armazenamento seguro e não são criptografados. Isso porque eles são destinados às preferências do usuário, como mencionado aqui no caso do iOS e aqui na documentação do Android ao falar sobre a biblioteca de segurança que foi projetada para fornecer wrappers para SharedPreferences especificamente para criptografar os dados antes de armazená-los.

Armazenamento seguro no celular

As únicas soluções de armazenamento seguro em dispositivos móveis são Keychain e KeyStore no iOS e Android, respectivamente, enquanto não há armazenamento seguro na Web .

O Keychain e o KeyStore são de natureza muito diferente: Keychain é uma solução genérica de armazenamento de credenciais, enquanto o KeyStore é usado para armazenar (e pode gerar) chaves criptográficas, chaves simétricas ou chaves públicas/privadas.

Isso significa que se, por exemplo, você precisar armazenar um token de sessão, no iOS você pode deixar o sistema operacional gerenciar a parte de criptografia e apenas enviar seu token para o Keychain , enquanto no Android é uma experiência um pouco mais manual porque você precisa para gerar (não codificar, isso é ruim) uma chave, use-a para criptografar o token, armazene o token criptografado em SharedPreferences e armazene a chave no KeyStore .

Existem diferentes abordagens para isso, assim como a maioria das coisas em segurança, mas a mais simples é provavelmente usar criptografia simétrica, pois não há necessidade de criptografia de chave pública, pois seu aplicativo criptografa e descriptografa o token.

Obviamente, você não precisa escrever um código específico de plataforma móvel que faça tudo isso, pois existe um plugin Flutter que faz tudo isso, por exemplo.

A falta de armazenamento seguro na Web

Esse foi, aliás, o motivo que me obrigou a escrever este post. Eu escrevi sobre o uso desse pacote para armazenar JWT em aplicativos móveis e as pessoas queriam a versão Web disso, mas, como eu disse, não há armazenamento seguro na Web . Não existe.

Isso significa que seu JWT precisa estar aberto?

Não, de jeito nenhum. Você pode usar cookies httpOnly , não pode? Esses não são acessíveis por JS e são enviados apenas para o seu servidor. O problema com isso é que eles sempre são enviados para o seu servidor, mesmo que um de seus usuários clique em um URL de solicitação GET no site de outra pessoa e essa solicitação GET tenha efeitos colaterais que você ou seu usuário não gostarão. Isso também funciona para outros tipos de solicitação, é apenas mais complicado. É chamado Cross-Site Request Forgery e você não quer isso. Está entre as ameaças à segurança da Web mencionadas nos documentos MDN da Mozilla, onde você pode encontrar uma explicação mais completa.

Existem métodos de prevenção. O mais comum é ter dois tokens, na verdade: um deles chegando ao cliente como um cookie httpOnly , o outro como parte da resposta. Este último deve ser armazenado em localStorage e não em cookies porque não queremos que seja enviado automaticamente para o servidor.

Resolvendo ambos

E se você tiver um aplicativo móvel e um aplicativo da Web?

Isso pode ser tratado de duas maneiras:

  1. Use o mesmo endpoint de back-end, mas obtenha e envie manualmente os cookies usando os cabeçalhos HTTP relacionados ao cookie;
  2. Crie um ponto de extremidade de back-end não Web separado que gere um token diferente de qualquer token usado pelo aplicativo Web e, em seguida, permita a autorização JWT regular se o cliente puder fornecer o token somente para dispositivos móveis.

Executando códigos diferentes em plataformas diferentes

Agora, vamos ver como podemos executar códigos diferentes em plataformas diferentes para poder compensar as diferenças.

Criando um plugin Flutter

Especialmente para resolver o problema de armazenamento, uma maneira de fazer isso é com um pacote de plug-ins: os plug-ins fornecem uma interface Dart comum e podem executar códigos diferentes em plataformas diferentes, incluindo código Kotlin/Java ou Swift/Objective-C específico da plataforma nativa . O desenvolvimento de pacotes e plugins é bastante complexo, mas é explicado em muitos lugares na Web e em outros lugares (por exemplo, nos livros do Flutter), incluindo a documentação oficial do Flutter.

Para plataformas móveis, por exemplo, já existe um plugin de armazenamento seguro, que é o flutter_secure_storage , para o qual você pode encontrar um exemplo de uso aqui, mas que não funciona na Web, por exemplo.

Por outro lado, para armazenamento de valor-chave simples que também funciona na Web, há um pacote de plug-in de primeira plataforma desenvolvido pelo Google chamado shared_preferences , que possui um componente específico da Web chamado shared_preferences_web que usa NSUserDefaults , SharedPreferences ou localStorage dependendo da plataforma.

TargetPlatform on Flutter

Após importar package:flutter/foundation.dart , você pode comparar Theme.of(context).platform com os valores:

  • TargetPlatform.android
  • TargetPlatform.iOS
  • TargetPlatform.linux
  • TargetPlatform.windows
  • TargetPlatform.macOS
  • TargetPlatform.fuchsia

e escreva suas funções para que, para cada plataforma que você deseja suportar, elas façam a coisa apropriada. Isso será especialmente útil para o próximo exemplo de diferença de plataforma, que são as diferenças em como os widgets são exibidos em diferentes plataformas.

Para esse caso de uso, em particular, também existe um plugin flutter_platform_widgets razoavelmente popular, que simplifica o desenvolvimento de widgets com reconhecimento de plataforma.

Exemplo 2: Diferenças em como o mesmo widget é exibido

Você não pode simplesmente escrever código multiplataforma e fingir que um navegador, um telefone, um computador e um smartwatch são a mesma coisa - a menos que você queira que seu aplicativo Android e iOS seja um WebView e seu aplicativo de desktop seja construído com Electron . Há muitas razões para não fazer isso, e não é o objetivo deste artigo convencê-lo a usar frameworks como o Flutter, que mantêm seu aplicativo nativo, com todas as vantagens de desempenho e experiência do usuário que o acompanham, permitindo que você escrever código que será o mesmo para todas as plataformas na maioria das vezes.

Isso requer cuidado e atenção, no entanto, e pelo menos um conhecimento básico das plataformas que você deseja oferecer suporte, suas APIs nativas reais e tudo isso. Os usuários do React Native precisam prestar ainda mais atenção a isso porque essa estrutura usa os widgets integrados do sistema operacional, então você realmente precisa prestar ainda mais atenção à aparência do aplicativo testando-o extensivamente em ambas as plataformas, sem poder alternar entre Widget iOS e Material em tempo real, como é possível com o Flutter.

O que muda sem o seu pedido

Existem alguns aspectos da IU do seu aplicativo que são alterados automaticamente quando você muda de plataforma. Esta seção também menciona o que muda entre Flutter e React Native a esse respeito.

Entre Android e iOS (Flutter)

O Flutter é capaz de renderizar widgets de material no iOS (e widgets de Cupertino (tipo iOS) no Android), mas o que ele NÃO faz é mostrar exatamente a mesma coisa no Android e no iOS: o tema do material se adapta especialmente às convenções de cada plataforma .

Por exemplo, animações e transições de navegação e fontes padrão são diferentes, mas não afetam tanto seu aplicativo.

O que pode afetar algumas de suas escolhas quando se trata de estética ou UX é o fato de que alguns elementos estáticos também mudam. Especificamente, alguns ícones mudam entre as duas plataformas, os títulos da barra de aplicativos estão no meio no iOS e à esquerda no Android (à esquerda do espaço disponível caso haja um botão voltar ou o botão para abrir uma gaveta (explicado aqui nas diretrizes do Material Design e também conhecido como menu de hambúrguer). Veja como é um aplicativo do Material com uma gaveta no Android:

imagem de um aplicativo Android mostrando onde o título da barra de aplicativos aparece nos aplicativos Flutter Android Material
Aplicativo de material rodando no Android: o título do AppBar está no lado esquerdo do espaço disponível. (Visualização grande)

E como é o mesmo, muito simples, aplicativo Material no iOS:

imagem de um aplicativo iOS mostrando onde o título da barra de aplicativos aparece nos aplicativos Flutter iOS Material
Aplicativo de material em execução no iOS: o título do AppBar está no meio. (Visualização grande)

Entre dispositivos móveis e Web e com entalhes na tela (Flutter)

Na Web há uma situação um pouco diferente, como mencionado também neste artigo sensacional sobre Desenvolvimento Web Responsivo com Flutter: em particular, além de ter que otimizar para telas maiores e levar em conta a maneira como as pessoas esperam navegar pelo seu site — que é o foco principal desse artigo — você precisa se preocupar com o fato de que às vezes os widgets são colocados fora da janela do navegador. Além disso, alguns telefones possuem entalhes na parte superior da tela ou outros impedimentos para a visualização correta do seu aplicativo devido a algum tipo de obstrução.

Esses dois problemas podem ser evitados envolvendo seus widgets em um widget SafeArea , que é um tipo específico de widget de preenchimento que garante que seus widgets caiam em um local onde possam ser exibidos sem que nada impeça a capacidade dos usuários de vê-los, seja uma restrição de hardware ou software.

Em Reagir Nativo

React Native requer muito mais atenção e um conhecimento muito mais profundo de cada plataforma, além de exigir que você execute no mínimo o iOS Simulator e o Android Emulator para poder testar seu aplicativo nas duas plataformas: não é o mesmo e converte seus elementos de interface do usuário JavaScript em widgets específicos da plataforma. Em outras palavras, seus aplicativos React Native sempre se parecerão com iOS - com elementos de interface do usuário Cupertino, como às vezes são chamados - e seus aplicativos Android sempre se parecerão com aplicativos Android normais do Material Design porque estão usando os widgets da plataforma.

A diferença aqui é que o Flutter renderiza seus widgets com seu próprio mecanismo de renderização de baixo nível, o que significa que você pode testar as duas versões do aplicativo em uma plataforma.

Contornando esse problema

A menos que você esteja procurando algo muito específico, seu aplicativo deve ter uma aparência diferente em diferentes plataformas, caso contrário, alguns de seus usuários ficarão insatisfeitos.

Assim como você não deve simplesmente enviar um aplicativo móvel para a web (como escrevi no post Smashing mencionado acima), você não deve enviar um aplicativo cheio de widgets de Cupertino para usuários do Android, por exemplo, porque será confuso para a maior parte. Por outro lado, ter a chance de realmente executar um aplicativo que possui widgets destinados a outra plataforma permite testar o aplicativo e mostrá-lo às pessoas em ambas as versões sem precisar usar dois dispositivos para isso necessariamente.

O outro lado: usando os widgets errados pelos motivos certos

Mas isso também significa que você pode fazer a maior parte do desenvolvimento do Flutter em uma estação de trabalho Linux ou Windows sem sacrificar a experiência de seus usuários do iOS e, em seguida, apenas criar o aplicativo para a outra plataforma e não precisar se preocupar em testá-lo completamente.

Próximos passos

As estruturas de plataforma cruzada são incríveis, mas transferem a responsabilidade para você, o desenvolvedor, de entender como cada plataforma funciona e como garantir que seu aplicativo se adapte e seja agradável de usar para seus usuários. Outras pequenas coisas a serem consideradas podem ser, por exemplo, usar descrições diferentes para o que pode ser essencialmente a mesma coisa se houver convenções diferentes em plataformas diferentes.

É ótimo não ter que construir os dois (ou mais) aplicativos separadamente usando linguagens diferentes, mas você ainda precisa ter em mente que está, em essência, construindo mais de um aplicativo e isso requer pensar em cada um dos aplicativos que você está construindo .

Recursos adicionais

  • O site da Galeria Flutter e o aplicativo Android, mostrando o uso de widgets Flutter típicos de diferentes plataformas e seu agnosticismo de plataforma
  • Documentação da API Flutter no TargetPlatform
  • Documentação do Flutter sobre a criação de pacotes e plugins
  • Documentação do Flutter sobre adaptações de plataforma
  • Documentação MDN sobre cookies