Escolhendo alvos de layout mobile por logs reais, não por chute de device
Uma lista de aparelhos parece precisa até o browser começar a dizer só Android 10; K ou apenas iPhone. Para layout, o nome exato do modelo era o alvo errado. O sinal útil era o fator de tela. Aqui está como transformamos um dia de logs de produção em uma matriz de layout mobile, comparamos contra referências públicas de 2026 e decidimos qual faixa o app precisa suportar.
O problema de desenhar para nomes de aparelhos
É tentador dizer que um app mobile deve ser testado numa lista de aparelhos populares: um Samsung A-series, um Moto G, um Redmi, um iPhone recente, talvez um iPhone SE antigo. A lista soa concreta. Também é frágil.
O nome do aparelho nem sempre aparece onde o problema de layout nasce. User agents de browser estão cada vez mais reduzidos. Uma fatia grande do tráfego Android web aparece como Android 10; K, o que quase não diz nada sobre o telefone físico. Tráfego de iPhone no browser muitas vezes diz apenas iPhone, sem modelo. Se você força essas linhas para devices exatos, o relatório fica mais bonito e menos verdadeiro.
A pergunta de layout que precisávamos responder era mais simples: quais formatos de tela o Daily Sudoku precisa aguentar sem cortar controles, esconder o tabuleiro ou deixar o jogador preso sem conseguir tocar no botão principal?
Isso é uma pergunta de proporção. Nas nossas notas chamamos essa proporção de factor: max(width, height) / min(width, height), arredondado para duas casas.
A parte incômoda é que isso não dá a mesma sensação de precisão que uma lista de aparelhos. Você não pode dizer “suportamos o iPhone 15 Pro Max” e encerrar. Precisa admitir quando uma linha é desconhecida, inferir apenas quando o dado permite, e comparar o tráfego local com alguma referência externa.
Foi a troca certa.
A fonte dos dados
Começamos com logs Loki de um endpoint de backend em produção. A query mirava requisições de criação de aposta e filtrava logs de request com user-agent. Amostramos o dia 15 de julho de 2026 em janelas de cinco minutos a cada meia hora, depois reduzimos cada linha para dois campos: timestamp e user_agent.
O arquivo bruto ficou em JSON lines. O relatório de análise agrupou cada linha por:
- nome do device
- nome do sistema operacional
- versão do sistema operacional
- resolução de tela quando conhecida ou inferida com segurança
- factor
- contagem de ocorrências
Tráfego de desktop e notebook saiu do relatório de layout. Isso removeu linhas classificadas explicitamente como desktop, Windows, macOS ou Linux. Tablets ficaram, porque ainda são superfícies móveis para esse app.
Depois desse filtro, o relatório mobile tinha 54.742 registros e 1.559 linhas agrupadas. Dessas ocorrências, 36.929 tinham tela conhecida ou inferida. 17.813 continuaram como unknown.
Esse unknown importa. Não é falha de limpeza; é proteção contra autoengano. Se o browser esconde o modelo, o relatório deve mostrar essa incerteza em vez de fingir que sabe a tela.
Por que factor foi mais útil que resolução
Resolução física é útil quando o modelo é conhecido. Ela é menos útil como alvo principal de design.
Por exemplo, todas estas resoluções caem no mesmo factor depois do arredondamento:
720x16001080x24001220x27121440x3200
Elas diferem em densidade de pixel, GPU, RAM e performance. Mas para formato de layout, estão todas perto de 2.22. Uma tela de Sudoku sente muito esse formato: tabuleiro, teclado numérico, controles, espaço de banner, modais e tela de vitória competem na vertical.
Com iPhones aconteceu algo parecido. Modelos recentes se concentram em 2.16 e 2.17. Inferir modelo exato a partir de um user-agent genérico iPhone seria desonesto, mas inferir factor para versões modernas de iOS ainda ajuda. Marcamos essas linhas como inferred-ios-modern, atribuímos factor 2.17, e deixamos o rótulo de tela explícito para ninguém confundir isso com resolução física real.
Essa diferença é o ponto central. Não precisávamos saber se um browser genérico em iOS 26 era um iPhone 16 Pro ou um iPhone 17 Pro Max. Precisávamos saber se ele pertencia ao bucket de telefone moderno alto.
O que o tráfego local mostrou
Depois de remover desktop e inferir iPhones genéricos modernos por factor, os principais grupos ficaram assim:
| Factor | Ocorrências | Participação |
|---|---|---|
2.22 |
18.000 |
32,88% |
2.17 |
9.823 |
17,94% |
2.16 |
2.487 |
4,54% |
2.23 |
2.394 |
4,37% |
2.24 |
1.183 |
2,16% |
unknown |
17.813 |
32,54% |
O maior bucket conhecido foi 2.22. Isso combina com formatos Android comuns: 720x1600, 1080x2400, 1220x2712 e telas altas parecidas. Os buckets mais associados a iPhone eram menores, mas relevantes: 2.16 e 2.17 cobriam uma parte importante da superfície iOS conhecida.
O bucket unknown ainda era grande demais para ignorar. A observação importante não foi “resolvemos todas as linhas”. Foi que as linhas conhecidas e as referências externas apontaram para o mesmo corredor: aproximadamente 2.16 a 2.24 em telefones modernos, com uma cauda legada menor perto de 1.78.
Isso já guia trabalho de layout.
A checagem externa
Logs de produção têm viés. Esse endpoint tem seu próprio público. Ele pode super-representar Android, uma região, um browser ou um fluxo específico. Então cruzamos com duas referências externas.
A primeira foi o StatCounter, em estatísticas de resolução mobile de junho de 2026. O StatCounter publica resolução de viewport ou CSS, não pixels físicos do aparelho. Não é correspondência um para um com a nossa tabela de resolução física. Mas o factor continua comparável porque o formato da tela é preservado.
Os principais fatores do StatCounter ficaram próximos:
| Factor | Participação mobile StatCounter |
|---|---|
2.22 |
21,61% |
2.17 |
19,37% |
2.16 |
18,45% |
2.23 |
2,89% |
2.24 |
2,11% |
A segunda referência foi a pesquisa de modelos de iPhone da TelemetryDeck. O CSV mostrava os dez modelos de iPhone mais usados na última semana disponível, atualizado em 3 de julho de 2026. Ao mapear esses modelos para resolução física, o top 10 caiu em apenas dois fatores:
| Factor | Participação no top 10 de iPhones |
|---|---|
2.17 |
78,92% |
2.16 |
21,08% |
Isso sustentou a inferência dos iPhones. iPhones recentes não se espalham por uma grande variedade de formatos. Para layout, tráfego genérico de iPhone moderno pode ser tratado como risco 2.16-2.17, com 2.17 como bucket conservador para os modelos atuais mais usados.
Transformando análise em alvo de layout
O relatório não era para virar uma tabela curiosa. Ele precisava mudar como testamos telas.
Para o Daily Sudoku, a decisão de produto ficou: as telas precisam funcionar em toda a faixa de factor 1.70 a 2.24.
Funcionar significa:
- controles críticos alcançáveis
- tabuleiro não cortado de forma que quebre o jogo
- modais concluíveis ou dispensáveis
- scroll disponível quando o conteúdo não cabe
- banners e safe areas sem cobrir ações principais
Isso não significa que todo factor recebe o mesmo polimento visual. O design base ainda deve favorecer o corredor de maior volume, especialmente 2.22, 2.17 e 2.16. Mas 1.70-2.24 virou a faixa mínima operacional. Fora dela, o app pode funcionar, mas não é o contrato atual de validação.
Isso pesa principalmente nas telas emocionais. A tela de vitória é o peak moment. Ela tem confete, pontuação, recompensa, botões de ação e às vezes pressão de monetização ou banner. Um layout que fica bonito só num simulador alto mas esconde “Novo Jogo” num telefone curto não está pronto.
O que mudou no workflow
Antes dessa análise, escolher viewport era fácil demais de discutir. Uma pessoa lembrava do próprio aparelho. Outra usava preset de simulador. Um bug report trazia um terceiro tamanho para a conversa. Nenhum desses sinais era errado, mas nenhum tinha peso.
Agora a sequência é mais clara:
- Comece pelo viewport reportado pelo usuário, se existir.
- Valide contra a faixa obrigatória de factor.
- Priorize polimento ao redor dos factors de maior volume.
- Mantenha unknown visível em vez de apagar.
- Compare o tráfego local com referência externa antes de transformar isso em regra permanente.
O workflow também mudou como lemos user agents. Nomes exatos de modelo são úteis quando existem, especialmente em webviews de app que adicionam metadados estruturados como device_name, os_name e os_version. User agents de browser exigem mais ceticismo. Android 10; K não é modelo. iPhone puro não é modelo. Eles continuam sendo tráfego real, mas devem aparecer como incerteza ou inferência segura de factor, não como precisão inventada.
É a mesma disciplina que usamos na critique heurística de UX do jogo React Native: transformar desconforto subjetivo em uma superfície ranqueada e testável. Aqui a superfície não eram heurísticas. Era formato de tela.
O que mudou quando implementamos o layout
A primeira implementação provou uma coisa importante: factor é alvo de validação, não unidade de layout.
Em certo momento tínhamos quatro telas de aba funcionando, mas cada uma carregava seu próprio conjunto de coeficientes pequenos: shortSide * 0.045 para padding de card em um lugar, shortSide * 0.065 para raio em outro, um mínimo e máximo um pouco diferente para ícones em uma terceira tela. Era melhor do que pixels fixos espalhados no JSX, mas ainda não era um sistema sustentável. Os números tinham mudado de lugar; eles ainda não explicavam intenção.
A correção foi manter os layouts privados por feature, mas mover a escala fluida das telas de aba para um ponto comum. O app agora tem uma escala de tabs com papéis nomeados: space, radius, text, icon e touch. Home, Favoritos, Loja e Perfil continuam mapeando essa escala para nomes locais, porque um card de desafio diário e um card de sequência do perfil não são o mesmo componente. Mas os ratios brutos e os limites de clamp vivem em um arquivo só.
Essa divisão importou. Não promovemos a escala para um pacote global de UI. Ela foi validada nas tabs do Sudoku, não no tabuleiro, nos modais de configuração, no fluxo de vitória ou em outro app. A fronteira chata deixou a mudança mais útil.
A segunda lição veio da tela de Configurações. O grid de botões de tema parecia ok no browser, mas quebrou visualmente em um emulador Android porque cada card ficou estreito demais para o texto em inglês. Colocar tudo em uma coluna resolvia a quebra, mas deixava a tela pesada. A correção melhor foi menor: manter duas colunas, limitar a descrição a duas linhas, dar altura mínima estável para cada card e centralizar o conteúdo verticalmente. O layout continuou denso sem deixar uma área morta no rodapé de cada botão.
Esse é o tipo de problema que a tabela de factor não resolve sozinha. A tabela diz quais formatos merecem validação. A interface ainda precisa ser inspecionada como conteúdo real, com strings reais.
Safe area não é só padding
O emulador Android também expôs uma armadilha de safe area. O header parecia distante demais do topo, como se alguém tivesse colocado um espaçador aleatório. A causa real era o display cutout. O emulador reportava uma área superior maior que a status bar normal, e o nosso shell inicialmente tratava o cutout inteiro como padding visual.
Em um Redmi Note 7 real, o mesmo código parecia razoável porque a área superior reportada era menor. No emulador, virava uma faixa vazia visível. Remover todo o padding acabava com a faixa, mas deixava o header colado demais no topo. A correção final foi deixar o shell das tabs responsável pela safe area e impedir que o conteúdo interno das abas adicionasse outro SafeAreaScreen. No Android, o padding do header respeita a altura real da status bar em vez de usar cegamente o display cutout inteiro.
A regra prática é simples: uma camada é dona do inset superior. Se o shell já cuida disso, as telas filhas não devem somar outra safe area. Quando duas camadas tentam ser seguras ao mesmo tempo, a interface desce até parecer quebrada.
Validamos isso com bounds do UIAutomator, não só com screenshot. No web, o primeiro bloco de Home, Favoritos, Loja e Perfil ficou na mesma posição vertical. No Android, o header começou onde o cutout reportado terminava. Isso deu uma resposta mensurável em vez de mais uma rodada de julgamento visual.
Produção web não é produção nativa
A mesma sessão pegou outro tipo de problema: o provider da Store. A configuração de produção usava corretamente RevenueCat para Android e iOS, mas o build web local de produção tentava inicializar o SDK nativo do RevenueCat e enviava erro para o Sentry: Invalid API key. Use your Web Billing API key.
O YAML não estava errado. Faltava o gate por plataforma. Produção Android deve usar RevenueCat. Produção web não deve inicializar o billing nativo com chave Android ou iOS. A configuração da Store agora troca revenuecat por um provider mock no web, mantendo RevenueCat nas plataformas nativas.
Isso também separou um problema real de preço de um artefato de mock. O mock da Store retornava um priceLabel falso como Test. Isso prova que um branch existe, mas é um valor ruim para a interface. Agora o mock retorna null para preço e deixa o app mostrar o fallback localizado de preço indisponível. No build de device, o logcat confirmou que o runtime nativo estava usando RevenueCat e recebendo preços reais em reais.
Isso ainda é trabalho de layout em sentido amplo. Um chip de preço escrito Test quebra confiança tanto quanto um botão cortado. UI responsiva não é só fazer caixas caberem na tela; também é garantir que o runtime escolha o provider certo para a superfície onde está rodando.
A resposta útil
A resposta útil não foi “suporte estes dez aparelhos”.
Foi esta:
Projete e teste o app ao redor do corredor de aspect ratio que aparece no tráfego real e nas referências públicas. Para este projeto, o corredor é dominado por 2.16-2.24, com 2.22 como bucket Android mais forte e 2.17 como bucket moderno de iPhone mais forte. Mantenha um caso legado mais curto perto de 1.78, e não esconda os unknowns.
Essa resposta é menos glamourosa que uma parede de aparelhos. Também é mais fácil de manter. Novos telefones chegam, user agents ficam mais privados, o modelo líder muda. O contrato de layout continua útil porque se baseia no formato que o app precisa aguentar, não no nome impresso numa ficha técnica.