NextRiot

Os Elementos da Engenharia de UI

December 30, 2018 • ☕️☕️ 9 min read

No artigo anterior, falei sobre admitir nossas brechas de conhecimento. Você pode chegar a conclusão que sugeri nos acomodarmos com a mediocridade. Eu não sugeri isso! Isso é uma área muito vasta.

Acredito fortemente que você pode “começar em qualquer lugar” e não necessite de aprender tecnologias em uma ordem específica. Mas também dou muito valor em ganhar perícia em algo. Pessoalmente tenho me interessado mais na criação de interfaces de usuário.

Tenho refletido sobre o que sei sobre e que considero valioso. Claro, estou familiarizado com algumas tecnologias (por exemplo JavaScript e React). Mas as lições mais importantes da experiência são elusivas. Nunca tentei colocá-las em palavras. Essa é minha primeira tentativa de listar e descrever algumas delas.


Existem diversas “trilhas de aprendizagem” sobre tecnologias e bibliotecas. Qual biblioteca estará em destaque em 2019? E em 2020? Devo aprender Vue ou React? Angular? E sobre Redux ou Rx? Devo aprender Apollo? REST ou GraphQL? É fácil se perder. E se o autor estiver errado?

Meus maiores avanços de aprendizado não foram sobre uma tecnologia específica. Ao invés disso, eu aprendia bem mais quando tinha dificuldades em resolver algum problema específico de UI. Às vezes, mais tarde eu descobria bibliotecas ou padrões que me ajudavam. Em outros casos, eu criava minhas próprias soluções (boas e ruins).

É essa combinação de entender os problemas, experimentar soluções e aplicar diferentes estratégias que me levaram às experiências de aprendizado mais recompensadoras. Esse artigo foca apenas nos problemas.


Se você já trabalhou com interfaces de usuário, provavelmente já teve de lidar com alguns desses desafios - seja diretamente ou usando uma biblioteca. Em ambos os casos, incentivo você a criar uma pequena aplicação sem bibliotecas e brincar reproduzindo e resolvendo esses problemas. Não há uma solução correta para nenhum deles. O aprendizado vem de explorar o escopo do problema e tentar diferentes possíveis soluções.


  • Consistência. Você clica em um botão “Curtir” e o texto atualiza para: “Você e mais 3 amigos curtiram esse post.” Você clica nele novamente e o texto retorna ao anterior. Parece fácil. Mas talvez um label assim está presente em diversos locais na tela. Talvez tenha uma outra indicação visual (como o fundo do botão) que deve ser alterado. A lista de “curtidores” que foi anteriormente obtida do servidor e está visível quando se passa por cima agora deve incluir seu nome. Se você navegar para uma outra página e voltar, o post não deve “esquecer” que foi curtido. Até mesmo a consistência local por si só cria uma lista de desafios. Mas outros usuários podem também modificar os dados que estamos mostrando (por exemplo ao curtir um post que estamos vendo). Como mantemos os mesmos dados sincronizados em diferentes partes da tela? Como e quando fazemos os dados locais serem consistentes com o servidor e vice-versa?

  • Responsividade. As pessoas podem tolerar a falta de feedback visual de suas ações apenas por um tempo limitado. Para ações contínuas como gestos e rolagem da págnia, esse limite é bem baixo. (Até mesmo um pular um frame de 16ms faz parecer “desajeitado”.) Para ações discretas como cliques, há pesquisas que dizem que os usuários acham que qualquer delay < 100ms é igualmente rápido. Mas há alguns desafios contra-intuitivos. Indicadores que fazem com que o layout “pule” ou que passe por diversos “estágios” de carregamento podem fazer com que a ação pareça mais longa do que foi. Da mesma forma, lidar uma interação em 20ms com o custo de descartar um frame de animação pode parecer mais lento que lidar com a mesma interação em 30ms sem descartas frames. Cérebros não são benchmarks. Como mantemos nossas aplicações responsivas para diferentes tipos de entradas?

  • Latência. Ambos, processamento e acesso à rede gastam tempo. Às vezes podemos ignorar o custo computacional se não atrapalhar a responsividade em nossos dispositivos alvo (não se esqueça de testar sua aplicação em um dispositivo de baixa performance). Mas lidar com a latência da rede é inevitável - pode levar segundos! Nossa aplicação não pode apenas parar esperando os dados ou código serem carregados. Isso quer dizer que qualquer ação que depende de novos dados, códigos ou recursos é potencialmente assíncrona e precisa de lidar com o “carregamento”. Mas isso pode acontecer em quase todas as telas. Como lidamos de forma elegante com a latência sem mostrar uma “cascata” de spinners ou “buracos” vazios? Como evitamos um layout “que pula”? E como trocamos as dependências assíncronas sem ter de “reconectar” nosso código toda vez?

  • Navegação. Esperamos que a UI se mantenha estável enquanto interagimos com ela. As coisas não devem desaparecer de baixo de nosso nariz. A navegação, seja iniciada dentro da aplicação (por exemplo clicando em um link) ou por um evento externo (por exemplo clicando no botão “voltar”), deve também respeitar esse princípio. Por exemplo, ao alternar entre as tabs /profile/likes e /profile/follows em uma página de perfil não deve limpar um campo de pesquisa fora da tab. Navegar para outra tela é como entrar em uma sala. As pessoas esperam voltar mais tarde e encontrar as coisas da forma que deixaram (com, talvez, alguns itens novos). Se você está no meio de um feed, clica em um perfil e depois volta, é frustrante perder sua posição no feed - ou esperar para que ele carregue novamente. Como podemos estruturar nossa aplicação para lidar com navegação arbitrária sem perder o contexto importante?

  • Obsolescência. Podemos fazer com que a navegação do botão “voltar” seja imediata se criarmos um cache local. Nesse cache, podemos “guardar” alguns dados para acesso rápido mesmo que pudéssemos teoricamente buscá-lo novamente. Mas utilizar cache trás seus próprios problemas. Cache pode ficar obsoleta. Se eu mudo um avatar, ele deve ser atualizado na cache também. Se eu criar um novo post, ele precisa de aparecer na cache imediatamente ou a cache precisará de ser invalidada. Isso pode se tornar difícil e fácil de dar erros. E se o post falhar? Quanto tempo a cache fica na memória? Quando buscamos novamente o feed, nós “emendamos” o novo feed com o que está em cache ou jogamos o da cache fora? Como a paginação ou ordenação é representada na cache?

  • Entropia. A segunda lei da termodinâmica diz algo como “com o tempo, as coisas tendem a se tornar desorganizadas” (bem, não exatamente). Isso se aplica a interfaces de usuário também. Não podemos prever as interações exatas do usuário nem sua ordem. Em qualquer momento, nossa aplicação pode estar em um de milhares de números de estados possíveis. Nós fazemos o melhor para que o resultado seja previsível e limitado ao nosso design. Não queremos olhar para um screenshot de um erro e pensar “como isso aconteceu”. Para cada N possíveis estados, existem N×(N–1) possíveis transições entre eles. Por exemplo, se um botão pode estar em um de 5 estados diferentes (normal, ativo, focado, perigo, desabilitado), o código que atualiza o botão deve estar correto para 5×4=20 possíveis transições - ou proibir algumas delas. Como domamos a explosão combinatória de possíveis estados e fazemos nosso resultado visual previsível?

  • Prioridade. Algumas coisas são mais importantes que outras. Um modal precisa de aparecer fisicamente “acima” do botão que o invocou e “se livrar” dos limites do seu container. Uma nova tarefa que foi agendada (por exemplo responder a um clique) pode ser mais importante que uma tarefa de longa duração que já se iniciou (por exemplo renderizar os próximos posts abaixo do limite da tela). Enquanto nossa aplicação cresce, partes do código escrita por pessoas e times diferentes podem competir por recursos limitados como processador, rede, estado da tela e o tamanho máximo do bundle. Às vezes podemos classificar os competidores em uma escala de “importância”, como a propriedade z-index do CSS. Mas dificilmente termina bem. Todo desenvolvedor tem a tendência de pensar que o seu código é importante. E se tudo é importante, então nada de fato é! Como fazemos que widgets independentes cooperem ao invés de brigarem por recursos?

  • Acessibilidade. Websites inacessíveis não são um problema de nicho. Por exemplo, no Reino Unido a incapacidade afeta 1 em cada 5 pessoas. (Aqui temos infográfico interessante.) Eu já senti isso pessoalmente também. Apesar de ter apenas 26 anos, eu tenho dificuldades em ler websites com fontes muito finas e com baixo contraste. Eu tento usar o trackpad com menos frequência e temo o dia que terei de navegar por sites mal implementados para teclados. Precisamos fazer com que nossas aplicações não sejam horríveis para pessoas com dificuldades - e a boa notícia é que há recursos de fácil acesso. Começa com estudos e com ferramentas. Mas também precisamos fazer com que seja fácil para os desenvolvedores do produto fazerem a coisa certa. O que podemos fazer para que a acessibilidade seja algo padrão ao invés de algo para ser pensado depois?

  • Internacionalização. Nossa aplicação precisa funcionar no mundo todo. As pessoas não apenas falam idiomas diferentes, mas também precisamos dar suporte a layouts “da direita-para-esquerda” (RTL: right-to-left) com o menor de esforço dos engenheiros do produto. Como podemos dar suporte a idiomas diferentes sem sacrificar a latência e a responsividade?

  • Entrega. Precisamos que o código de nossa aplicação chegue ao computador do usuário. Qual tipo de transporte e formato utilizamos? Isso pode parecer simples, mas há diversas compensações. Por exemplo, aplicações nativas tendem a carregar todo seu código antecipadamente ao custo de um tamanho enorme de aplicação. Aplicações web tendem a ter um carregamento inicial menor ao custo de maior latência durante o uso. Como escolhemos em qual ponto iremos introduzir a latência? Como otimizamos nossa entrega baseada nos padrões de uso? Qual tipo de dado precisamos para uma solução otimizada?

  • Resiliência. Você pode gostar de insetos (bugs) se você for um entomologista, mas provavelmente não irá gostar de vê-los em seus programas. Contudo, alguns erros irão inevitavelmente chegar em produção. O que acontece então? Alguns erros causam um comportamento errado, mas bem definido. Por exemplo, talvez seu código exiba um resultado visual incorreto em alguma condição. Mas e se o código de renderização “quebrar”? Então não podemos continuar de forma significativa porque o resultado visual seria inconsistente. Uma falha ao renderizar um único post não deveria “estragar” um feed inteiro ou deixá-lo em um estado “semi-quebrado” que causaria mais erros. Como escrevemos código de forma que conseguimos isolar a renderização e a busca de falhas e que mantenha o restante da aplicação em execução? O que significa tolerância a falhas para interfaces de usuário?

  • Abstração. Em uma aplicação pequena, podemos escrever código específico para lidar com diversos casos especiais dos problemas acima. Mas aplicações tendem a crescer. Queremos ser capazes de reutilizar, dividir e juntar partes do nosso código e trabalhar nele coletivamente. Queremos definir limites claros entre as peças familiares para diferentes pessoas e evitar fazer com que a lógica que muda com frequência seja muito rígida. Como criamos abstrações que escondem detalhes de implementação de uma parte específica da UI? Como evitamos reintroduzir os mesmos problemas que acabamos de resolver quando nossa aplicação crescer?


Claro, há diversos problemas que eu não mencionei. Essa lista de forma alguma está completa! Por exemplo, Não falei sobre a colaboração entre o design e a engenharia, ou depuração e testes. Talvez de uma próxima vez.

É tentador ler sobre esses problemas com uma biblioteca de UI ou uma biblioteca de busca de dados em mente como solução. Mas eu o incentivo a fingir que essas bibliotecas não existem e ler novamente dessa perspectiva. Como você iria lidar com a solução desses problemas? Teste elas em uma aplicação pequena (Eu amaria ver seus experimentos no GitHub - sinta-se à vontade para enviar um tweet para mim em resposta.)

O interessante sobre esses problemas é que a maioria deles aparecem em qualquer escala. Você pode os encontrar em pequenos widgets como um autocomplete ou um tooltip e também em grandes aplicações como o Twitter e o Facebook.

Pense em um elemento incomum de UI de uma aplicação que você gosta de utilizar e o analise com essa lista de problemas. Você consegue descrever alguns dos tradeoffs escolhidos pelos desenvolvedores? Tente recriar um comportamento semelhante do zero!

Aprendi muito sobre engenharia de UI experimentando esses problemas em pequenas aplicações sem utilizar bibliotecas. Recomendo o mesmo para todos que queiram ganhar uma apreciação aprofundada pelos tradeoffs da engenharia de UI.