5 temas técnicos importantes na criação de aplicativos mobile
Este post é parte de uma série de publicações relacionadas ao que chamamos de mindset mobile. Ou seja, falamos sobre um modo de pensar e desenvolver específicos para aplicativos mobile, que por sua vez traz desafios diferentes quando comparados a web sites ou APIs.
A primeira parte desta série aborda características dos smartphones e a segunda parte aborda comportamentos de usuários (em breve). Nesta parte abordaremos 5 temas técnicos importantes ao desenvolver aplicativos mobile. Vamos falar sobre os seguintes temas:
- Retrocompatibilidade
- Segurança
- Migrações
- Feature Flags
- Entrega
São pontos que consideramos importantes e que aplicamos, no dia a dia, em um projeto real e complexo.
Retrocompatibilidade
Quando é feito o release de uma nova versão de um aplicativo, pode-se levar vários dias até que os usuários atualizem. Uma parte dos usuários, inclusive, pode nunca atualizar um aplicativo se assim preferir. Isso significa que, ao longo do tempo, diferentes versões estarão instaladas nos smartphones. Tendo isto em mente, é necessário garantir que os serviços de back-end continuem dando suporte ao contrato existente.
"Mas o que é contrato, neste caso?"
Um contrato, de forma simplificada, pode ser entendido como o formato reconhecido nos dados trocados entre o aplicativo e o serviço back-end.
Suponhamos que o serviço back-end retorne o seguinte dado mediante uma requisição:
{
"id": 1,
"title": "Desafios técnicos"
}
Suponhamos, agora, que seja necessário renomear o campo title
para name
.
O que aconteceria se o campo title
fosse renomeado para name
? O aplicativo deveria ser alterado para utilizar o novo nome do campo, correto? Porém, existe uma versão anterior do aplicativo que espera um campo chamado title
. Esta versão já está nas mãos de diversos usuários. Ela vai parar de funcionar corretamente caso o campo seja renomeado no serviço.
Como proceder? Neste caso, existem três estratégias:
- Subir uma nova versão do serviço com o novo contrato e manter ambos serviços em execução, em diferentes URLs.
- Manter o campo novo e o antigo no mesmo serviço.
- Forçar uma atualização do aplicativo para a versão que usa o novo contrato.
As opções 1 e 2 são ideais do ponto de vista de experiência de usuário, pois os mesmos poderão seguir utilizando o aplicativo normalmente, sem serem obrigados a atualizar. Porém, geram uma necessidade de maior manutenção do serviço back-end. A opção 3 possui um menor custo de manutenção, mas vai afetar a experiência do usuário. Vamos falar sobre ela daqui a pouco.
É importante, em qualquer caso, elaborar testes automatizados que impeçam que os contratos existentes não sejam quebrados. Imagine que, por um engano, uma pessoa apague ou renomeie o campo title
? Como impedir tais erros?
Uma ferramenta específica para isto é o Pact. Ela permite gerar arquivos que representam os contratos a partir dos testes automatizados do Consumer — neste contexto, o aplicativo que utiliza os dados do serviço. O serviço, chamado de Provider, realiza testes usando os arquivos de contrato gerado pelo Consumer para verificar que seu formato está adequado.
Outra maneira, menos elegante, seria executar uma bateria de testes de integração (entre aplicativo e serviço) para cada versão ainda ativa em produção.
Voltando à opção 3 … o que seriam as atualizações forçadas?
Existem casos onde uma versão que está em produção apresenta problemas. Pode ser um bug crítico ou o esforço de manter a retrocompatibilidade. Em todo caso, é importante que exista desde o primeiro release um mecanismo no qual o aplicativo se torne “inoperante” e exija uma atualização.
Uma possível forma de se resolver esta questão é implementar um “serviço de compatibilidade”.
Este serviço back-end deve receber como input qual a versão e dispositivo do aplicativo. Algo similar a isto:
{
"agent": "android",
"version": "1.0.0"
}
Se você se perguntou por que é necessário enviar o tipo de aplicativo (iOS, Android, WindowsPhone, etc), a razão é a seguinte: é comum que um mesmo serviço back-end seja utilizado por diferentes aplicativos. E é provável que cada um tenha uma versão diferente. Se é esse o caso, é importante diferenciar. Caso exista um serviço back-end separado por aplicativo, não é necessário enviar o tipo.
Como output, deve retornar o resultado desta validação:
{
"compatible": true
}
Em caso de que seja incompatível, o aplicativo deve apresentar ao usuário uma tela que solicita uma atualização, pois a versão se tornou obsoleta. É conveniente inserir um botão que leva o usuário diretamente para a loja.
Segurança
Segurança é um tema muito amplo. Existe documentação extensa sobre como se implementar mecanismos de segurança em aplicações Android e iOS. Além da documentação oficial, existe também o guia OWASP específico para aplicações mobile. Recomendamos a leitura.
Para falar de certos temas de segurança, vamos imaginar algumas situações.
Situação 1: um usuário do seu aplicativo se conectou em um Wi-Fi público de uma cafeteria muito famosa e movimentada. Em seguida, ali mesmo, abriu o aplicativo, fez o login, consultou e adicionou informações importantes. Esses dados foram lidos e salvos em um serviço back-end. Como garantir que esses dados estão protegidos?
Uma forma de proteger os dados de usuários é utilizar apenas conexões seguras com serviços back-end. O uso de SSL deve estar presente em todas requisições, assim prevenindo que o conteúdo das mesmas seja interceptado por um agente mal intencionado. Ou seja, os dados serão obtidos e enviados de forma encriptada.
Além de utilizar apenas conexões seguras, uma medida extra de segurança é utilizar um recurso chamado “Certificate Pinning”, que consiste em incorporar no aplicativo o mesmo certificado utilizado no servidor externo. Isto garante que o aplicativo identifique e apenas se conecte com o servidor legítimo, e não um "impostor".
Isto previne ataques do tipo man-in-the-middle, também conhecido como MITM ou “ataque do homem do meio”, no qual a comunicação direta entre o aplicativo e o servidor legítimo é interceptada por um terceiro que rouba (ou até modifica) as informações trocadas.
Situação 2: um usuário que utiliza seu aplicativo modificou o sistema operacional do seu celular para poder instalar aplicativos não-oficiais. E esse usuário instalou, sem querer, um aplicativo malicioso que tenta roubar dados.
Uma medida de segurança extra é necessária quando dados de usuários ficam guardados dentro do smartphone. É importante que estes estejam criptografados. Ou seja, os dados ficam legíveis apenas para o aplicativo correto. Quando um terceiro tenta obter acesso ao arquivo com os dados, estes são inacessíveis.
Situação 3: uma pessoa mal intencionada quer descobrir como o seu aplicativo funciona através da leitura do código-fonte para encontrar brechas de segurança ou informações sensíveis.
Uma vez que consiga ler o código (através de técnicas de engenharia-reversa), essa pessoa poderá obter, por exemplo:
- Endereços de serviços back-end
- Regras de negócio
- Código de funcionalidades ainda não liberadas (que podem estar "em construção")
- Senhas ou tokens de acesso (péssima prática, não faça isto)
Neste caso, é necessário fazer a ofuscação de código. Este mecanismo torna mais difícil a tarefa de descompilar o código-fonte e entender seu exato funcionamento. Uma máxima a se considerar aqui é: nunca armazene dados sensíveis, como senhas, no código-fonte. Mesmo acrescentando camadas adicionais de segurança, não há como evitar que strings sejam extraídas (como as senhas e tokens citados acima).
O Android oferece uma opção chamada ProGuard. Essa ferramenta torna o código ilegível sem alterar seu funcionamento na maioria dos casos. Entretanto, existem cenários específicos que podem apresentar problemas, por isto é necessário testar todas as funcionalidades após fazer a ofuscação. Com respeito a aplicativos iOS, não existe uma solução nativa para ofuscação. Em ambos os casos, existem ferramentas externas, pagas e gratuitas, que fazem o processo de ofuscação.
Vale ressaltar que este processo pode tornar mais difícil o debug de aplicações e exigir mudanças de código ou arquiteturais para não quebrar as funcionalidades existentes.
Migrações
Com o passar do tempo, é normal que um aplicativo passe a armazenar mais dados do que inicialmente projetado. Aplicativos que utilizam bases de dados provavelmente precisarão lidar com migrações em algum momento.
Além de criar testes unitários das migrações, é importante executar testes integrados que validem as migrações, seja automatizado ou não.
Uma forma de se testar este comportamento é:
- Instale a versão A (provavelmente a versão que está em produção no momento)
- Adicione dados no aplicativo (preferencialmente aqueles que tem uma migração envolvida e que poderiam ser afetados)
- Instale a versão nova do aplicativo sem apagar os dados (as migrações ocorrerão na inicialização do mesmo)
- Verifique que os dados seguem persistidos corretamente
Este teste garante que usuários não perderão seus dados ao atualizar e pode prevenir um cenário muito pior: que o aplicativo feche imediatamente após ser aberto, um erro comum caso hajam falhas tanto no script de migração como depois do mesmo.
Feature Flags
É importante que determinadas funcionalidades possam ser ativadas ou desativadas quando necessário, por parte da equipe mantenedora do aplicativo, de forma automática, rápida e massiva, sem que isto exija que usuários atualizem o aplicativo.
O motivo pode ser tanto técnico como de negócio. No caso de problemas técnicos, podemos citar lentidão, instabilidades ou erros diversos. Motivos de negócios podem incluir, por exemplo, datas específicas para uma funcionalidade ou até questões legais que afetam uma regra de negócio.
Quando uma determinada funcionalidade de um aplicativo apresenta problemas, é importante ter um mecanismo para desativá-la e impedir que usuários entrem no fluxo. Permitir que usuários utilizem uma funcionalidade “quebrada” pode afetar negativamente a satisfação do aplicativo.
Uma forma de resolver este problema é implementar um serviço externo que informa para o aplicativo o estado das “flags” (ou “toggles”). Pode-se atualizar toggles ao abrir o aplicativo, ao abrir uma nova tela, ou a cada requisição ao serviço back-end.
É conveniente que as toggles sejam diferenciadas por ambiente, versão e sistema operacional. Ou seja, pode ser que uma funcionalidade de Login esteja afetando apenas o aplicativo Android na versão mais recente, e não na anterior. Elas podem se parecer com algo assim:
{
"login_ios_v1": true,
"login_ios_v2": true,
"login_android_v1": true,
"login_android_v2": false
}
A razão para se ter toggles por ambiente é que se uma determinada toggle foi desativada em ambiente de produção, devido a um incidente, isto não deve interferir em um teste de integração que é executado em ambiente separado.
Entrega
Fazer o release de um aplicativo traz desafios diferentes daqueles de serviços web. É um processo que exige um pouco mais de planejamento e esforço. Vale ressaltar que as lojas Google Play e App Store possuem algumas similaridades neste contexto.
Primeiramente, é necessário que o aplicativo não viole as regras. Existe um conjunto de diretrizes a respeito do desempenho, design, aspectos legais, segurança dos usuários, privacidade, entre outros. Por exemplo: um aplicativo que pode induzir usuários a danos físicos, como uma calculadora para medir dosagem de remédios, pode ser impedido de ser publicado.
É necessário também ter uma atenção especial com os metadados. Os textos e screenshots que são enviados a loja devem ser fiéis às funcionalidades e estarem atualizados. No caso da Apple, em específico, as pessoas que revisam os aplicativos a cada release precisam de informações precisas sobre como utilizar o aplicativo. Pode ser necessário enviar dados de login e senha, por exemplo, e um tutorial passo-a-passo para realizar certas operações. Não explicar bem como utilizar o aplicativo pode resultar em uma rejeição do release.
Este cenário faz com que as releases não sejam um processo trivial, meramente técnico. É necessário dedicar um esforço adicional para pensar na comunicação adequada com os usuários.
Equipes que adotam processos de entrega contínua podem ficar tranquilas: é possível fazer. Mas é um pouco diferente. Neste caso, a entrega contínua chegará até a etapa de distribuição: ou seja, todo commit que atravessa toda a pipeline pode chegar nas mãos de "usuários beta". É possível configurar grupos de usuários beta no TestFlight, no caso de um aplicativo iOS, e na Google Play, tratando-se de aplicativo Android.
O release para lojas, portanto, se torna um processo pontual, feito periodicamente de acordo com as necessidades do projeto. Pode ser automatizado utilizando uma ferramenta chamada Fastlane.
Basta clicar em "Publicar" e pronto? Está no ar?
Infelizmente, não. O processo de revisão da App Store pode demorar. É importante se planejar com antecedência e não fazer um lançamento "em cima da hora" quando existe uma data específica para que esteja disponível o aplicativo. De acordo com a Apple, mais de 90% dos aplicativos são revisados em até 48 horas, mas existem casos nos quais uma revisão pode demorar mais de duas semanas (isto não é mencionado no site, mas sabemos de casos assim). Existe a possibilidade de solicitar uma revisão urgente, mas não é garantido que vão revisar no tempo solicitado.
No caso da Google Play, a revisão é mais rápida e costuma levar algumas horas apenas. É feita uma verificação automática que, somente em certos casos, enviam para uma pessoa revisar.
Sabemos que os desafios de desenvolver aplicações mobile são muitos e existem outros temas que não citamos. Esperamos que esse conhecimento que compartilhamos seja útil para você e sua equipe.
Agradecimentos
Este post teve seu conteúdo revisado por Larissa Barra, Leandro Alonso, Lílian Dias, Marcella Silva, Paulo Gama e Phellipe Silva. Vocês são demais, obrigado!