Building Mobile Apps At Scale
Quando o problema é simples de resolver, soluções simples atendem. Porém, quando os problemas se tornam complexos é preciso de soluções escaláveis. Não existe bala de prata mas é preciso negociar escolhas técnicas.
PARTE 1: Desafios Devido à Natureza das Aplicações Móveis
Desenvolvimento móvel tem várias peculiaridades e desafios únicos, abaixo alguns deles:
1. Gestão do Estado
- A rede pode falhar e gerar falhas e estados corrompidos
- Armazenamento do estado em cache
2. Erros são difíceis de reverter
- A atualização das versões da loja pode ser demorada
3. A cauda longa das versões antigas de aplicativos
- Múltiplas versões podem rodar em paralelo
- Pode quebrar com o backend
4. Links diretos
- Pode quebrar em versões antigas do app
- Problemas de estado corrompidos
- As implementações de Deeplink são diferentes para iOS (links universais e esquemas de URL) e para Android (com base em intenções).
5. Notificações push e em segundo plano
- Entrega não é garantida (sinal, silenciamento…)
- Configurar e operar notificações push é complexo. Tanto para Android quanto para iOS, seu aplicativo precisa obter um token de um servidor (FCM no Android, APNS no iOS) e, em seguida, armazenar esse token no backend. Há muitas etapas a serem seguidas para que as notificações push funcionem;
6. Falhas no aplicativo
- A primeira regra para travamentos é que você precisa monitorar quando eles acontecem e ter informações de depuração suficientes.
7. Suporte off-line
- Detectando com segurança quando o telefone está offline
- Detectando velocidade e latência de conexão
- Persistência do estado local quando o dispositivo está offline e sincronização novamente quando a conexão for recuperada.
8. Acessibilidade
- Acessibilidade é crucial em apps populares, pois muitos usuários têm necessidades de acessibilidade e há risco legal se o app não for acessível. Além disso, apps acessíveis têm melhor qualidade.
- O nível de profundidade na implementação das diretrizes WCAG 2.1 mobile deve ser confirmado inicialmente.
- Uso por voiceover/talkback;
- Contraste de cores/elementos
- Permitir preferências como tamanho de fonte é importante.
- A fragmentação de dispositivos precisa ser considerada também.
Implementar acessibilidade desde o início tem esforço surpreendentemente baixo em iOS e Android. Retrofit é mais trabalhoso, por isso é melhor incluir acessibilidade no processo de design e planejamento.
Testes de acessibilidade precisam ser planejados. Automatizar o possível, testar manualmente regularmente, recrutar usuários de acessibilidade para feedback e ativar recursos durante desenvolvimento são boas práticas.
9. CI/CD e o trem de construção
- Nas grandes empresas ter um tempo de infra para cuidar do CI/CD é importante.
- Para empresas menores usar fornecedores como Bitrise é a melhor escolha devido ao trabalho.
- Mantendo a branch master green em escala
10. Bibliotecas e SDKs de terceiros
- Responsabilidade sobre a segurança
- Feature Flag para desativá-lo
- Tamanho do aplicativo
- Está bem mantido com atualizações, suporte…
11. Fragmentação de dispositivos e sistemas operacionais
- Diferença de sistema operacional (APIs e funcionalidades), além de diferenças de hardware (telas, processador, memória)
12. Compras no aplicativo
- RevenueCat facilita a criação e o gerenciamento de compras no aplicativo iOS e Android em grande escala.
- Estados In App Purchase - IAP (cancelamento, recompra, atualização…), avaliações, descontos, problemas com cartão de crédito…
PARTE 2: Desafios devido à complexidade do aplicativo
13. Arquitetura de navegação em aplicativos grandes
Ter uma estratégia de navegação de aplicativo bem definida com boa separação do estado do aplicativo é fundamental para qualquer aplicativo de tamanho decente.
14. Estado do aplicativo e alterações orientadas a eventos
- Siga as práticas recomendadas de gerenciamento de estado para manter baixo o número de bugs que ocorrem devido a problemas de mudança de estado. - - --- Mantenha o estado o mais imutável possível e armazene modelos como objetos imutáveis que emitem mudanças de estado.
- Registre estados inválidos com informações para reproduzir ou depurar, para ter detalhes sobre o que deu errado e como reproduzir o problema.
15. Internacionalização
Tanto o iOS quanto o Android oferecem maneiras opinativas de implementar a localização. O iOS oferece suporte à exportação de localizações para tradução, enquanto o Android é baseado em strings de recursos. As ferramentas são um pouco diferentes, mas o conceito é semelhante.
Para localizar seu aplicativo e definir as strings, você deseja localizar e enviar as strings localizadas como um recurso separado no binário. Ainda assim, com aplicativos grandes e muitas localidades, você rapidamente enfrenta desafios com esse fluxo de trabalho. De outro lado, você pode escolher usar a tradução local, mas isso dificulta a atualização no lado nativo.
16. Arquitetura Modular e Injeção de Dependência
A injeção de dependência é uma ferramenta poderosa para manter código testável de forma consistente em toda a base de código.
Conway's law diz que a estrutura de um sistema de software reflete a estrutura da organização que o desenvolveu. Isso significa que se a empresa que cria o software está dividida em vários times ou departamentos, o software também provavelmente será dividido em partes correspondentes aos times. Por exemplo, imagina que a empresa tem um time de designers, um time de programadores e um time de testadores. Então o software que eles criam juntos provavelmente terá uma parte de design, uma parte de programação e uma parte de testes que correspondem a esses times. É como se você e seus amigos decidirem construir uma casa na árvore. Se cada amigo fizer uma parte diferente da casa, como a escada, o telhado e as paredes, no final as diferentes partes se encaixarão como uma casa completa, mas você ainda poderá ver quais partes foram feitas por cada amigo. Então essa é a ideia básica do Conway's law - a estrutura de um sistema de software acaba refletindo a estrutura da organização que o criou! Me pergunte se você tiver qualquer outra dúvida.
17. Testes automatizado
- Se você não está realizando um nível decente de testes automatizados em um aplicativo grande, então você está cavando um buraco para si mesmo.
- Teste unitário: o mais simples de todos os testes automatizados, testando um componente isolado, também chamado de “unidade”.
- Os testes de integração são um avanço em complexidade em relação aos testes unitários.
- Teste de instantâneo: compara o layout de um elemento ou página da interface do usuário com uma imagem de referência.
- Teste de UI: um teste que exercita a UI e testa se a UI se comporta de uma maneira específica. https://go.mobileatscale.com/unit-testing-benefits
18. Testes manuais
Testes manuais são viáveis inicialmente, porém não escalam. Automatize a maioria dos testes, mas ainda faça testes manuais exploratórios e para casos como pagamentos e câmera. Incorpore testes manuais no fluxo de build/release.
PARTE 3: Desafios devido a grandes equipes de engenharia
19. Planejamento e Tomada de Decisão
Formalizando o processo de planejamento
- Processo de planejamento RFC (Request for Comments é o esculacho da idéia) (https://blog.pragmaticengineer.com/scaling-engineering-teams-via-writing-things-down-rfcs/)
- PRD (documento de requisitos do produto)
- Equipes iOS e Android trabalhando juntas no planejamento
- Paralisia de decisão As equipes podem sofrer de paralisia de decisão. Limite o tempo de planejamento e escolha a abordagem mais sensata. Prototipar e obter feedback pode ser mais importante do que planejamos.
- Equilibrando sinal e ruído (Equilibrando sinal e ruído) Equipes temem criar “ruído” com novos planos. Porém, trabalhar em silos traz vantagens. Documentos níveis de design e RFCs ajudam a espalhar o conhecimento rapidamente. Comece definindo modelos. Consiga a adesão dos engenheiros. Publique os documentos para todos e itere com base nas respostas
20. Arquitetando maneiras de evitar pisar nos pés uns dos outros
Arquitetura não importa muito inicialmente. O problema começa quando muitos engenheiros modificam os mesmos arquivos. Para escalar a centenas de engenheiros:
- isole features
- use estrutura monorepo
- defina ownership forte
- tenha testes automatizados.
Documente a abordagem e considere ferramentas para aplicá-la.
21. Arquitetura compartilhada entre vários aplicativos
Times inicialmente constroem apps com arquiteturas diferentes. A idéia de unificar a arquitetura surgirá com o tempo. Rewrites completos são caros e dolorosos. Um meio termo sensato é fazer mudanças incrementais em direção a uma arquitetura unificada, sem reescrever tudo. Benefícios incluem linguagem compartilhada, planejamento em conjunto, quebrar silos, e componentes compartilhados. Introduza novos conceitos em novas partes do app. Rewrites completos raramente fazem sentido, a não ser que grandes mudanças no app sejam necessárias.
22. Maturidade de ferramentas para grandes equipes de engenharia
Em apps com milhões de linhas de código e dezenas de engenheiros, ferramentas nativas começam a ter problemas de performance e processo. Tempo de build é um dos maiores gargalos. Otimizar o build e workflows vale a pena em grande escala. Comparado ao backend, ferramentas para apps grandes ainda são um problema menos resolvido. Considere construir suas próprias ferramentas além do disponível prontamente. Isso ajuda com tempo de build, workflow de releases e muito mais.
23. Dimensionando tempos de construção e mesclagem
O tempo de build de apps nativos pode se tornar problemático conforme os projetos crescem. Engenheiros iOS estão familiarizados com a lentidão das builds do Xcode e o mesmo ocorre no Android. Apple e Google não priorizaram melhorias nessa área para projetos grandes.
Felizmente existem ferramentas para acelerar o tempo de build, como o Bazel, adotado por empresas como Uber, Pinterest e Grab. Ainda assim, é trabalho integrar e configurar essas ferramentas. Quanto mais engenheiros móveis, mais faz sentido investir em melhorar a experiência de build.
Em certo ponto, equipes consideram migrar de repositórios distribuídos para monorepo para evitar downloads constantes. Não existem ainda boas ferramentas de monorepo para mobile, então customização é necessária.
Manter o branch master sempre funcionando também se torna desafiador. Se os builds são rápidos e poucos merges ocorrem por dia, não há problema. Porém, com builds demorados e muitos PRs por hora, é complexo manter o master estável e o tempo de merge baixo. Na Uber, construímos uma Submit Queue para lidar com isso, dividindo builds em paralelo e priorizando mudanças com mais chance de passar.
24. Bibliotecas e equipes de plataformas móveis
Conforme o número de engenheiros móveis cresce, a tendência de "reinventar a roda" emerge, com diferentes equipes criando suas próprias implementações de funcionalidades como logging e armazenamento de dados.
Bibliotecas internas móveis são criadas para compartilhar essas funcionalidades, mas a manutenção se torna difícil com o tempo. Os engenheiros originais podem sair, a qualidade diminui com fixes rápidos e a equipe dona não tem bandwidth para mudanças maiores.
Equipes de plataforma móvel são uma solução comum para gerenciar bibliotecas compartilhadas. Elas assumem áreas como build, release, ferramentas, arquitetura, desempenho, confiabilidade e SDKs. Não há regra para quando criar esse time, mas 20-30 engenheiros móveis é comum.
Começar o time de plataforma cedo demais pode ser um desafio para justificar, tirando engenheiros experientes das equipes de produto. Tarde demais resulta em redundância e pouca reutilização. Equilibrar esses tradeoffs é chave.
PARTE 4: Linguagens e abordagens multiplataforma
25. Adotando novas linguagens e estruturas
Adotar uma nova linguagem ou framework é arriscado, especialmente em apps complexos. A mudança para o Swift no Uber quase foi um desastre devido ao aumento drástico no tamanho dos binários. Após isso, o Uber passou a avaliar novas tecnologias com mais cuidado.
Áreas para avaliar incluem maturidade da linguagem/framework, necessidade de migrações, entusiasmo dos engenheiros, riscos e construção de um projeto piloto. Meu conselho é ser receptivo a novas tecnologias, mas limitando o "raio de explosão" - experimentando em partes menos críticas do app ou com menos usuários primeiro.
A evolução mobile é constante e histórias como a mudança para o Swift servem de alerta para ter um "plano B" ao adotar novas tecnologias.
26. Kotlin Multiplatform e KMM
O Kotlin Multiplatform e o Kotlin Multiplatform Mobile (KMM) são abordagens destacadas para o desenvolvimento multiplataforma, permitindo o compartilhamento eficiente de código em Kotlin.
O Kotlin Multiplatform, apresentado em 2017, suporta a criação de bibliotecas JVM, frameworks nativos e artefatos JavaScript.
O KMM, lançado em 2020, oferece ferramentas que simplificam o desenvolvimento para iOS e Android, incluindo integração rica na IDE e suporte ao Cocoapods.
Embora empresas como Netflix e Square tenham obtido sucesso, existem desafios relacionados à relativa imaturidade da tecnologia em 2021, com ferramentas experimentais e possíveis mudanças impactando os primeiros usuários.
No entanto, acredito no potencial do Kotlin Multiplatform e do KMM, especialmente para os engenheiros nativos do Android, proporcionando uma transição suave e uma curva de aprendizado acessível para os desenvolvedores iOS.
27. Desenvolvimento de recursos multiplataforma
A ideia por trás dos recursos multiplataforma e da lógica de negócios: escrever a lógica independente da plataforma uma vez e reutilizá-la nos aplicativos
28. Desenvolvimento de aplicativos multiplataforma versus nativo
Motivações a serem consideradas para uma abordagem de desenvolvimento de aplicativos multiplataforma:
- A “necessidade de velocidade”. A frustração com o tempo que pode levar para construir um recurso em ambas as plataformas. Não seria ótimo trabalhar com uma abordagem que promete um tempo de desenvolvimento de recursos mais rápido, e isso acontece em ambas as plataformas?
- O desejo de ter um engenheiro embarcando para duas plataformas, em vez de precisar de dois engenheiros. Para implementar as alterações mais simples na interface do usuário, dois engenheiros precisam fazer duas alterações separadas em duas plataformas. Ambos precisam ser testados e as implementações muitas vezes precisam ser coordenadas. Não seria bom se a mesma pessoa pudesse fazer as duas coisas?
- O desejo de que os aplicativos iOS e Android funcionem exatamente da mesma forma. Os aplicativos iOS e Android para o mesmo produto geralmente diferem em alguns aspectos. Um bug estará presente no Android, mas não no iOS. Não seria ótimo que ambos os aplicativos funcionassem de maneira idêntica?
- Unificando a aparência dos aplicativos iOS e Android. Com o tempo, a equipe de design defenderá uma abordagem UI/UX compartilhada. Isso fará muito sentido tanto do ponto de vista da marca quanto da redução do trabalho de design de duas plataformas para uma plataforma unificada.
- O desejo de “recarregar a quente”, em vez de esperar pela construção de trens. Mesmo a menor alteração no aplicativo móvel leva semanas para ser enviada devido às alterações de código que precisam ser enviadas pela App Store. Não seria maravilhoso ter a opção de experimentar ou enviar correções de bugs sem ter que esperar pelo processo de construção?
29. Web, PWA e Backend-Driven Mobile Apps
Não poder atualizar apps nativos em tempo real é a fonte de muitas dores, como visto nos capítulos sobre revertendo erros e versões de app. Felizmente, existem algumas "varinhas mágicas" para atualizações instantâneas:
- PWAs (Progressive Web Apps) usam APIs web para experiência nativa-like. Úteis para aprimorar sites móveis, mas não substituem apps nativos complexos.
- Webviews embutidos permitem conteúdo dinâmico, mas há desafios como performance e UX não nativa. Requer esforço alto em otimização.
- Apps guiados por backend enviam lógica executável ou metadados que controlam o app nativo. Contorna limitação de não poder atualizar binários facilmente. Porém, tem riscos como Apple proibir código executável e é desafiante de versionar e testar.
Em geral, cada abordagem tem prós e contras. PWAs complementam sites. Webviews embutidos habilitam conteúdo dinâmico. Backend guiando o app contorna atualizações limitadas, mas adiciona complexidade. Equilibrar tradeoffs é importante.
Discussão sobre Backend Driven
PARTE 5: Desafios devido à intensificação do seu jogo
30. Experimentação
Empresas com apps relevantes fazem testes A/B mesmo em pequenas mudanças, para medir impacto e evitar regressões negativas. Isso vai além de feature flags e envolve rollout controlado, análise, detecção de problemas e pós-análise.
Em pequena escala é fácil, mas em grande escala como o Uber com 1000+ testes simultâneos, é complexo. Ferramentas ajudam, mas sistemas in-house são comuns em grandes empresas para casos de uso específicos, controlar os dados e por ser core do negócio.
Motivações para soluções de fornecedores são custo (mais barato que construir e manter próprio) e padronização (ao invés de times diferentes com soluções customizadas).
Empresas menores/médias tipicamente usam soluções prontas como Firebase Remote Config, LaunchDarkly e Optimizely. Já grandes empresas constroem próprias.
Outro desafio é processos para rastrear testes e evitar que se impactem. Isso requer ferramentas e processos customizados. Testar tudo é comum em larga escala para evitar regressões significativas. Na Uber, até fixes eram testados para garantir métricas de negócio.
Em resumo, testes A/B são poderosos mas desafiadores em grande escala. Times pequenos usam soluções prontas, grandes constroem próprias. Bons processos também são necessários além de ferramentas.
Vendors: Firebase Remote Config, LauchDarkly, Optimizely, Split.io and Amplitude
31. O Inferno de Feature Flag
Feature flags são ótimas para testes A/B, rollouts graduais e segmentados. Porém, problemas surgem conforme o número de flags ativas cresce:
- Dependências entre flags podem complicar rollouts. Mapear dependências ajuda.
- Flags conflitantes entre equipes quebram parte do app quando ativadas em conjunto.
- Flags obsoletas permanecem no código como código morto, pior do que código morto normal por ser difícil confirmar. Automatizar limpeza ajuda.
- Códigos inconsistentes em volta de flags dificultam limpeza automatizada. Padrão consistente e linter ajudam.
Soluções prontas de vendors são maduras, porém sistemas customizados são um grande esforço a não ser que haja caso de negócio forte. Construir uma fachada sobre soluções prontas pode ser alternativa smart.
Em resumo, flags são valiosas mas geram dívida técnica. Controlar isso requer disciplina, padrões e, idealmente, automatização da limpeza.
32. Performance
- Tempo de inicialização do aplicativo inchado;
- Muitas chamadas de rede paralela;
- Desempenho de rede: Uber criou seu próprio protocolo de rede;
- Taxa de consumo da bateria;
- Aplicativo não responde (ANR);
- Quadros congelados e quadros de renderização lenta;
- Desempenho de animações e renderização de IU;
33. Análise, monitoramento e alertas
Monitoramento e alertas para problemas são práticas comuns em backend e web, mas menos em mobile. Relatórios de crash são o alerta mais comum em mobile. Porém, monitoramento de eventos de negócio e alertas são raros.
Passos para um bom monitoramento:
- Definir eventos de negócio críticos como fluxos de pagamento ou cadastro
- Mapear esses eventos nas plataformas móveis
- Implementar monitoramento em tempo real desses eventos
- Certificar métricas com especificações claras e testes automatizados
- Validar dados comparando fontes e fazendo verificações
Para alertas:
- Alertas de crash são mais comuns, mas limitados
- Alertas em métricas-chave durante rollouts ajuda a detectar regressões
- Poucos times fazem alertas em eventos de negócio, apesar do valor
- Ruído é um problema, limiares e dados regionais complexos de acertar
Em resumo, monitoramento e alertas de eventos de negócio móvel é poderoso, mas desafiador. Requer trabalho em definir, mapear e validar dados, além de lidar com ruído e dimensionalidade.
Vendors: Amplitude, Flurry, Mixpanel, Fullstory, Appsee, Datadog, Sentry
34. Mobile On-Call
Ter uma escala de sobreaviso mobile se torna necessário quando você tem alertas móveis, mesmo que apenas de crashes. Equipes grandes conseguem ter uma escala só de mobile. Equipes pequenas acabam com poucas pessoas no sobreaviso.
Ter a escala é só o primeiro passo. Treinar a resposta a incidentes é crucial para mitigação rápida e impacto mínimo. Isso envolve priorização, escalonamento, divisão de tarefas e lições aprendidas.
Times com poucos engenheiros mobile acabam mesclando o sobreaviso com backend. Isso só funciona se houver runbooks simples e claros para a maioria dos alertas. Sem eles, engenheiros não saberão como responder.
Em resumo, ter uma boa escala de sobreaviso mobile requer pessoas suficientes, treinamento em respostas a incidentes, runbooks abrangentes e investimento para aprender com cada incidente.
35. Verificações avançadas de qualidade de código
Static Analysis Ter feedback rápido sobre problemas de código aumenta a produtividade de engenheiros e times. Checagens avançadas de código fornecem isso antes mesmo do code review.
Formatação de código via linting garante que o código siga guia de estilo. Regras mais avançadas de lint aplicam padrões de arquitetura e codificação. Análise estática (static analysis) inspeciona automaticamente em busca de problemas mais complexos como variáveis não utilizadas e possíveis nulos. Ferramentas populares incluem SwiftLint, ktlint, SonarQube, Clang analyzer e Infer. Equipes grandes constroem próprias.
Cobertura de código (code coverage) também ajuda mostrando o quanto código é testado. Integrado ao workflow, isso força política mínima de cobertura.
- Prós: feedback rápido, maior qualidade e estabilidade.
- Contras: tempo de integração e manutenção.
Equilibrar valor x esforço das ferramentas é chave.
36. Compliance, Privacidade e Segurança
Apps e processos de desenvolvimento geralmente precisam seguir regulamentações e diretrizes de privacidade. As mais comuns são:
- PII (Informações de Identificação Pessoal) não podem ser acessadas por ninguém além dos autorizados.
- GDPR (Regulamento Geral de Proteção de Dados) da UE expande o escopo de PII e seu uso.
- Diretrizes específicas da indústria como PCI DSS para pagamentos ou HIPAA para saúde.
Áreas de impacto em engenharia mobile:
- Log de dados precisa ser anonimizado e criptografado se sensível.
- Auditar partes do app como SDKs de terceiros para conformidade com GDPR e PII.
- Treinar engenheiros sobre implicações de leis de privacidade no código.
- Checagens de segurança no CI/CD ajudam, assim como treinamento específico em riscos.
Em resumo, conformidade e privacidade são cruciais e exigem trabalho extensivo em processos, código e auditoria. Quanto antes revisar, melhor.
37. Migração de dados no lado do cliente
Apps móveis que armazenam dados localmente enfrentam desafios únicos de migração de schema na atualização. Migrar grandes conjuntos de dados localmente é complexo. Dificuldades incluem testar a migração com dados reais, testar upgrades de versões antigas, logging no cliente e lidar com falhas. Bugs no novo schema também podem travar usuários. Idealmente, o backend deveria ser a fonte da verdade em mudanças de schema para evitar migrações frágeis no dispositivo.
38. Atualização forçada
A maioria dos apps maduros hoje em dia implementa mecanismos de atualização forçada. Motivações incluem aposentar APIs antigas, reduzir custos de teste e suporte, remover bugs severos, e corrigir vulnerabilidades. Implementações existem no Snapchat, Facebook Messenger, bancos, JustEat e outros. O desafio é construir a solução bem antes de precisar usá-la. Testar também é crucial para garantir que funcionará. Google oferece atualizações no app nativamente no Android 5.0+. No iOS, soluções próprias são necessárias. Suportar modelos de celular antigos é complicado. A estratégia depende do caso de uso do negócio. Alguns apps suportam versões antigas por anos, outros definem limite. Atualização forçada não é só ferramenta, mas estratégia. Cobrir edge cases é chave.
39. Tamanho do App
Tamanho do app importa porque impacta downloads e remoções. Apps menores têm mais downloads. No Android, bundles reduzem 35% do tamanho. No iOS, limite é 200MB via OTA. Uber teve enorme queda ao passar esse limite.
Apps "Lite" pequenos são estratégia comum em mercados emergentes. Uber, Facebook e Google os usam. Requer tradeoffs como mais esforço de engenharia, funcionalidade reduzida e otimizações de rede e assets.
Em grandes apps, ativos estáticos inflam o tamanho se não monitorado. Plataformas móveis costumam assumir monitoramento e redução de size. Size instalado também é importante, mas raramente monitorado.
Mesmo não sendo visível, tamanho grande impacta métricas. Vale investir em otimização se o impacto no business for significativo. Do contrário, não vale o esforço.