Logo do Venturus
Mobile: Por que o paradigma declarativo está dominando o mercado?
  • 28 de fevereiro de 2023
  • Blog

Mobile: Por que o paradigma declarativo está dominando o mercado?

Este artigo é a segunda parte da série: O paradigma declarativo e por que ele vai dominar o ambiente de desenvolvimento Mobile. Leia o primeiro artigo para entender o paradigma declarativo!

Espero que vocês tenham tido tempo de pensar em nossa pergunta: Por que o paradigma declarativo está dominando o ambiente Mobile? O que o tornou tão necessário ao desenvolvimento?

Não existe uma resposta exata para essa pergunta, mas, aqui, vamos descrever um problema que com certeza impactou o desenvolvimento Mobile com o passar dos anos.

 

Interface de usuário

Existem muitos motivos que fazem com que a programação declarativa esteja cada vez mais presente no ambiente de desenvolvimento Mobile, mas nesta série vamos focar a análise em um problema específico: as interfaces de usuário, nossas famosas UIs.

Quando o desenvolvimento Mobile teve início, nós tínhamos poucos tipos de aparelhos, poucos formatos de tela e a programação imperativa atendia bem às necessidades dos desenvolvedores.

Com o tempo, cada vez mais celulares e tecnologias diferentes foram surgindo e hoje os desenvolvedores precisam criar códigos que atendam aparelhos com os mais variados formatos de tela — telas dobráveis, telas curvas, com cortes na tela para câmera etc.

Com isso, imperativamente, foi ficando cada vez mais difícil atender a todos os requisitos.

Outro ponto importante é que os desenvolvedores hoje precisam lidar com uma expectativa crescente dos usuários com aplicativos.

Cada vez mais, os usuários esperam ver apps bonitos, interativos, rápidos. No paradigma imperativo, cada vez é maior o trabalho dos desenvolvedores para atender aos requisitos, criar animações, controlar componentes.

Um último ponto importante é que há uma necessidade de mercado de criar soluções cada vez mais integradas. Ou seja, os usuários esperam que os aplicativos preferidos deles funcionem nos celulares, mas também em tablets, computadores, TVs e relógios.

Agora que entendemos nosso problema, vamos nos aprofundar no porque é tão difícil atender a todos esses requisitos.

 

UI imperativa

Vamos tentar descrever em alto nível o processo de criar uma tela de usuário imperativamente. Vale ressaltar que, apesar de ser um processo imperativo, o desenvolvimento nativo já possuía ferramentas para descrever a tela, mas não para interagir com ela.

No Android, possuímos os layouts em XML (que, como vimos, é uma DSL) e, no iOS, possuímos as Storyboards, que funcionavam também descritivamente com os Xibs. Mas, quando queríamos interagir com alguns componentes, nós precisávamos obter uma referência desses componentes e acessar métodos e propriedades no nosso código.

Vamos tomar como exemplo uma tela de login comum em muitos aplicativos. Essa tela é descrita em algum ponto de código por uma ferramenta declarativa. Mas precisamos receber as entradas do usuário, como o que ele digitou nos campos de login e senha. Para isso, o que fazemos é obter uma referência desses componentes em nosso código e adicionar listeners, ler as informações do campo, alterar propriedades etc.

Lembremos que essa tela de login não possui apenas os campos de login e senha. Ela possui também um botão de login, um botão de novos usuários e um para pessoas que esqueceram a senha. Para interagirmos com esses componentes, repetimos o mesmo processo: obtemos uma referência dos elementos e adicionamos listeners para receber interações.

É fácil perceber que, quanto mais componentes precisamos, mais difícil é manter e entender o código gerado. Além disso, começamos a criar cenários em que nossos componentes dependem dos estados de outros componentes: um botão de Login que só pode ser habilitado se algo foi digitado pelo usuário; ou um campo de texto a ser mostrado caso a senha esteja inválida.

O GIF abaixo descreve bem a macarronada que podemos gerar com referências de tantos componentes e tantas dependências de estados para serem controladas por uma tela.

 

GIF mostra um smartphone (UI) que se comunica com diversos retângulos (Views) à esquerda ligados por setas bidirecionais

 

Além disso, como dissemos anteriormente, com uma expectativa crescente dos usuários, cada vez mais componentes são necessários para atender os requisitos de design. Outro ponto é que, com tantas dependências, temos um código de difícil leitura — em que é fácil para o desenvolvedor esquecer de atualizar algum estado — e também de difícil manutenção.

Por fim, temos ainda um cenário de sobrecarga de estados, em que tanto nosso código que manipula os componentes quanto nossa UI declarada possuem estados definidos e configurados. Assim, fica mais difícil entendermos qual o estado atual de cada componente.

Vamos analisar agora as etapas do desenvolvimento Mobile e qual foi a solução encontrada para facilitar a vida dos desenvolvedores.

 

A pilha de desenvolvimento

Antes de falarmos sobre a solução encontrada, vamos falar um pouco sobre a pilha, ou Stack, de desenvolvimento com a qual nós normalmente lidamos ao desenvolver para aparelhos móveis.

Os sistemas operacionais Mobile possuem camadas de código que compõe o sistema operacional. Nós temos uma camada de Kernel rodando no baixo nível, temos nossas abstrações de Hardware, as famosas HAL. Temos também uma camada de código nativo que se comunica com a HAL para expor sensores e funcionalidades.

Uma camada de Framework expões serviços do sistema e funcionalidades necessárias para o desenvolvimento dos apps. E, por fim, temos a camada de Aplicativos, em que a maioria dos desenvolvedores está mais ambientada.

 

GIF com as camadas de código. De baixo para cima: Kernel, HAL, Código nativo, Framework, Aplicacações, com uma camada pontinhada e transparente caindo por cima de todas

 

É na camada de aplicações que fazemos o desenvolvimento dos nossos aplicativos. É nela que acessamos e manipulamos recursos, obtemos interações do usuário, controlamos fluxos e requisições.

Todo o desenvolvimento das UI imperativas descritas anteriormente é feito nessa camada, acessando recursos e componentes do sistema para criar e interagir com essa interface do usuário.

Nesse contexto, o que os desenvolvedores propuseram foi: por que nós não criamos uma camada extra, algum tipo de DSL, que possua alta expressividade no domínio de criar interface de usuário, abstraindo toda essa interação direta com referências dos componentes?

Dessa forma, os desenvolvedores passariam a interagir com essa DSL por meio de estados e a DSL cuidaria do papel de manipular as referências e memórias por baixo dos panos.

Dito tudo isso, vamos descrever em alto nível o processo de criar UI declarativas.

 

UI declarativa

Declarativamente, nós continuamos tendo nossa UI, mas o que faremos é encapsular toda a manipulação de referências em uma DSL, uma linguagem de domínio específico que visa ter alta expressividade na criação e manipulação das interfaces de usuário. Agora, definimos um estado e descrevemos como a tela deve ser manipulada com aquele estado.

 

 

Com isso, conforme o estado muda, nossa DSL é informada e ela mesma cuida de manipular e gerar uma nova tela baseada nas informações que descrevemos para aquele estado.

Imagem mostra a correspondência entre a mudança do estado (Estado 2) e a respectiva alteração na UI (UI 2)

 

É como se nossa tela inteira fosse gerada do zero toda vez que um estado muda. E é de se imaginar que seria um processo muito custoso redesenhar a tela inteira toda a vez que houvesse uma interação do usuário ou uma animação a ser exibida.

Por isso, os Frameworks que temos hoje para desenvolvimento de UI declarativa passam por um longo processo de aprimoramento de performance antes de serem oficialmente lançados. Por exemplo, o Jetpack Compose, ferramenta da Google, ficou 3 anos em desenvolvimento até a primeira versão estável ser lançada.

Portanto, com o uso dessas DSLs que encapsulam a lógica de controle dos componentes, o desenvolvedor passa a ficar responsável apenas por definir o que deve acontecer e não mais como, gerando códigos mais sucintos, reutilizáveis, legíveis e de fácil manutenção.

Vamos agora ver exemplos de pseudo-código de 4 FW de UI declarativa para desenvolvimento Mobile.

 

Exemplos de códigos

Para entender melhor os benefícios dessa abordagem, vamos primeiro analisar 4 códigos de Frameworks diferentes para desenvolvimento de UI declarativa. Escolhemos uma tela muito simples que pode ser vista na imagem abaixo:

 

Exemplo de tela. Contém um ícone azul que indica usuário ou foto, um nome (José da Silva) e, abaixo, uma cidade (Campinas - SP)

 

A tela é um cartão de usuário que possui 3 componentes: um campo de imagem do usuário a esquerda e 2 campos de texto com o nome e cidade, um em cima do outro mais a direita. Nos exemplos de código, vamos analisar como a organização dos elementos na tela é feita. Os Frameworks que serão analisados serão:

  • Jetpack Compose: biblioteca recente da Google para desenvolvimento nativo para Android em Kotlin.
  • SwiftUI: biblioteca lançada pela Apple para desenvolvimento nativo para iOS em Swift.
  • Flutter: kit de desenvolvimento multiplataforma, desenvolvido também pela Google baseado na linguagem
  • React Native: biblioteca criada pelo Facebook e com funcionamento muito similar ao irmão React para desenvolvimento multiplataforma em Javascript.

Sem mais delongas, vamos aos códigos!

 

Jetpack Compose:

Pedaço de código em Jetpack Compose descrevendo a tela com foto, nome e cidade

 

SwiftUI:

Pedaço de código em SwiftUI descrevendo a tela com foto, nome e cidade

 

 

Flutter:

Pedaço de código em Flutter descrevendo a tela com foto, nome e cidade

 

React Native:

Pedaço de código em Reactive Native descrevendo a tela com foto, nome e cidade

 

Analisando esses códigos, podemos ver grandes similaridades entre todos eles. Apesar de cada um ser escrito em uma linguagem, de terem sido lançados em épocas diferentes e por empresas diferentes, todos possuem uma certa lógica em comum para controle dos componentes e estados.

No Compose declaramos nosso componente usando funções de composição. No SwiftUI, com as chamadas View structs. No Flutter, estendendo as classes de Widget. E, no React Native, com essa declaração de constantes.

Todas elas representam uma forma de criar um componente de tela reutilizável! As diferenças estão muito mais ligadas às sintaxes e ferramentas disponíveis na linguagem do que propriamente a lógica de controle.

E as semelhanças não param por aí! Podemos ver também que todas elas possuem mecanismos em suas próprias DSLs para organização dos elementos. Algumas chamam de Row e Column, outras de HStack e VStack ou até mesmo as “Flex Views” no React Native.

Outro ponto importante é que elas já possuem os principais componentes usados pelos desenvolvedores disponíveis também em suas DSLs: imagens, textos, campos de imput do usuário, botões, animações, espaçamentos, listas infinitas e muitas outras funcionalidades.

Com todas essas semelhanças, o desenvolvedor que é especializado em um desses Frameworks consegue aprender a migrar para outro FW com mais facilidade.

Na próxima parte de nossa série, vamos falar um pouco sobre o futuro do desenvolvimento Mobile e nossa visão sobre como essas novas ferramentas podem mudar a forma como desenvolvemos projetos nativos.