Conhecendo o Jetpack Compose

Daniely Murua
wearejaya
Published in
13 min readJun 8, 2023

Mas o que é Jetpack Compose?

Jetpack Compose é conjunto de ferramentas inovadoras para criar interfaces de aplicativos nativos no Android. Até recentemente, os layouts em projetos nativos eram criados usando XML. Embora muitos projetos ainda sigam essa abordagem, o Compose está se tornando cada vez mais popular, sendo adotado por grandes empresas. No entanto, essa transição traz desafios, além do simples layout de uma tela… mas vamos por partes.

Neste artigo, vou explicar as partes essenciais do funcionamento do Jetpack Compose. Você aprenderá os fundamentos dessa tecnologia revolucionária. Vamos mergulhar nas principais características e entender como o Jetpack Compose pode transformar a maneira como desenvolvemos aplicativos nativos Android. Prepare-se para descobrir os segredos por trás dessa poderosa ferramenta!

Vantagens

Primeiramente, quais as vantagens de utilizar o Jetpack Compose? Vejamos algumas delas:

  • Simplicidade: O Jetpack Compose simplifica o desenvolvimento de interfaces de usuário no Android, permitindo que você crie belas UIs de forma mais fácil e rápida.
  • Kotlin em vez de XML: Com o Jetpack Compose, você pode escrever suas interfaces usando Kotlin, uma linguagem moderna e concisa, em vez de depender de XML complexo e verboso.
  • Alta performance: O Jetpack Compose utiliza um mecanismo de renderização eficiente que oferece uma ótima performance, proporcionando uma experiência fluida e responsiva aos usuários.
  • Menos código: Com a abordagem declarativa do Jetpack Compose, você precisa escrever menos código para construir sua UI. Isso significa menos boilerplate e mais produtividade no desenvolvimento.
  • Ferramentas poderosas: O Jetpack Compose vem com um conjunto de ferramentas poderosas para auxiliar no desenvolvimento, como o Compose Preview, que permite visualizar as alterações em tempo real, e o Compose Inspector, que ajuda a depurar e inspecionar a hierarquia da interface.
  • Uso intuitivo das APIs do Kotlin: O Jetpack Compose aproveita as funcionalidades poderosas do Kotlin, permitindo que você utilize recursos como extensões, lambdas e tipos nulos para criar uma UI de forma mais intuitiva e expressiva.
  • Interface guiada por estado (State Driven UI): Com o Jetpack Compose, você pode criar interfaces orientadas pelo estado, onde as alterações nos dados automaticamente atualizam a UI. Isso simplifica o gerenciamento de estados e facilita a criação de interfaces dinâmicas e interativas.

Composable Functions

A Composable Function é uma parte essencial do Jetpack Compose. Elas desempenham um papel fundamental na definição da interface de usuário. Tudo o que você deseja exibir na tela do seu aplicativo deve ser colocado dentro dessas funções.

@Composable
fun ComposableFunctionName() {
...
}

Cada Composable Function representa um componente ou uma parte específica da interface, e elas podem ser combinadas ou aninhadas para criar a estrutura completa da tela. Isso torna as funções altamente reutilizáveis e flexíveis.

@Composable
@Preview
fun MyScreenPreview() {
MyScreenContent()
}

@Composable
fun MyScreenContent() {
Column {
Text(text = "Hello, Jetpack Compose!")
MyComponent()
}
}

@Composable
fun MyComponent() {
// Your component code here
}

É importante destacar que apenas outras Composable Functions podem chamar uma Composable Function. Essa hierarquia de chamadas entre as funções permite construir o layout da interface de forma organizada e modular.

Composable functions são declarativas, o que significa que você descreve como a UI deve ser renderizada com base no estado dos dados, e o Jetpack Compose atualiza automaticamente a UI quando esse estado muda.

Como mostrado nos exemplos acima, para que uma função seja reconhecida como Composable Function, basta adicionar a anotação @Composable logo acima da função. Essa anotação indica ao Jetpack Compose que a função é responsável por definir a interface de usuário.

Por trás, um plugin compilador do Kotlin é responsável por transformar essas funções Composable em elementos de interface de usuário compreensíveis pelo sistema Android. Esse processo ocorre durante a compilação do código e permite que as funções sejam convertidas em elementos nativos do Android.

Preview

Uma funcionalidade incrivelmente útil do Jetpack Compose é a capacidade de visualizar o seu componente ou tela em tempo real, antes mesmo de executar o aplicativo. Para isso, basta adicionar a anotação @Preview acima da função desejada. O preview do componente será exibido na seção "Split", do ambiente de desenvolvimento, permitindo que você visualize instantaneamente as alterações feitas no seu código. Essa função agiliza o processo de desenvolvimento, pois você pode iterar e ajustar o design da interface de forma rápida e eficiente.

@Composable
@Preview
fun MyComponentPreview() {
MyComponent()
}

@Composable
fun MyComponent() {
// Your component code here
}
Preview de um componente
Preview da tela com aninhamento de composables

Design

O Jetpack Compose suporta completamente os princípios do Material Design, permitindo que você utilize temas customizados e defina elementos essenciais como cores, tipografia e formatos. Isso possibilita criar uma experiência visual consistente e alinhada com os padrões do Material Design. Com o Jetpack Compose, você tem total controle sobre a aparência da sua interface, podendo personalizar e ajustar os temas de acordo com as necessidades do seu aplicativo.

Além disso, o Jetpack Compose oferece uma vasta seleção de componentes pré-construídos que seguem as diretrizes do Material Design, simplificando a criação de interfaces atraentes e intuitivas.

@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
darkTheme -> DarkColorScheme
else -> LightColorScheme
}

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
setContent {
MyTheme() {
MyApp() //Composable function
}
}

Outra vantagem interessante é a facilidade de habilitar o modo escuro (dark theme) no Jetpack Compose. Com apenas algumas configurações, você pode oferecer aos usuários a opção de alternar entre o tema claro e o tema escuro em seu aplicativo, como mostra no código acima.

Comportamento

Como mencionado anteriormente, o Jetpack Compose utiliza funções composable para transformar dados em interfaces de usuário. Esse processo é conhecido como Composition. Durante a composição, as funções composable são executadas para construir a hierarquia da UI com base nos dados fornecidos. É nesse momento que os elementos visuais são criados e atualizados de forma reativa, garantindo que a interface reflita as alterações nos dados em tempo real.

"Composition is a description of the UI built by Jetpack Compose when it executes composables."

No entanto, ao criar nosso layout, é comum que certos elementos precisem ser alterados. Por exemplo, podemos querer mudar o estado de um botão de “habilitado” para “desabilitado” enquanto uma carga está sendo processada na tela.

Quando usamos XML para definir nosso layout, estamos basicamente instruindo o sistema sobre como as visualizações devem ser renderizadas em um determinado estado. Definimos as propriedades e configurações das visualizações no XML, e o sistema se encarrega de apenas renderizá-las conforme essas especificações.

No entanto, com o Jetpack Compose, a abordagem é um pouco diferente.

Todos os elementos de interface do usuário são representados por funções, em vez de objetos. Isso significa que não há referência direta aos elementos em si, e não podemos chamá-los diretamente para fazer alterações. Portanto, ao invés de definir manualmente as configurações das visualizações em XML, usamos as funções composable, que são controladas por estados e parâmetros. Isso segue um padrão reativo, onde as funções composable são atualizadas automaticamente com base nas mudanças nos estados e parâmetros.

Portanto, quando usamos o Jetpack Compose, podemos atualizar o estado do botão e, consequentemente, sua aparência será atualizada automaticamente para refletir essa mudança. Não precisamos nos preocupar em especificar explicitamente como a visualização deve ser renderizada em cada estado, pois o Jetpack Compose lida com isso de forma automática e reativa.

Essa é uma das principais diferenças entre o uso do XML e o Jetpack Compose, tornando este último muito mais intuitivo. No XML, manipular manualmente as views para lidar com estados de erro, por exemplo, pode ser uma abordagem arriscada, deixando espaço para possíveis bugs ou comportamentos inesperados. Imagine se esquecermos de desabilitar um botão de compra em um cenário específico que não foi previamente mapeado?

Lidar com essas mudanças de estado pode ser um desafio significativo usando XML, mas o Jetpack Compose resolve esse problema. Legal, né? Vamos tentar visualizar essa diferença na prática.

Vamos construir um botão que irá incrementar o número exibido no texto do botão e ao chegar no 10, o botão deve ficar desabilitado, como mostra o gif abaixo.

Como faríamos isso usando XML? Pensando de uma forma bem simples, teríamos que ter nosso XML em um arquivo separado e em nossa activity/fragment obter a instância do botão. Feito isso, adicionar um listener para escutar evento de clique do botão e dentro dele incrementar o count, habilitar/desabilitar o botão quando necessário e alterar o texto, tudo isso se referindo a instância de objeto criado a partir do XML.

val count = 0

fun onClickButton() {
count++
button.enable = count < 10
button.setText("Value: ${count}")
}

O que aconteceria se a variável “count” tivesse o valor 10 e, em um cenário não previsto, fosse redefinida? O texto ainda exibiria o valor “10” e o botão ficaria desabilitado, impedindo a interação do usuário. Agora, imagine se o layout pudesse reagir a qualquer mudança no estado “count”…

Vamos dar uma olhada no Compose:

@Composable
fun MyButton() {
val count = mutableStateOf(0)

Button(
onClick = { count.value++ },
enabled = count.value < 10
) {
Text(text = "Value: ${count.value}")
}
}

Temos uma variável mutável "count" que começa com o valor padrão 0, e em seguida, construímos o botão, especificando o que deve ser feito quando o botão é clicado, quando ele deve ser habilitado/desabilitado e o texto que o componente deve exibir. Todos esses elementos dependem de um único estado chamado “count”.

De forma simples, um estado em um aplicativo Android é um valor que pode ser alterado ao longo do tempo. Pode ser um valor de um banco de dados, uma variável em uma classe, ou qualquer outra fonte de dados. Esse estado é responsável por determinar o que a interface do usuário irá exibir em um determinado momento e como ela irá se comportar.

No exemplo mencionado, o “count” é o estado. Conforme esse estado é alterado, a função composable correspondente é acionada. No entanto, para que isso funcione corretamente, é necessário que o estado seja mutável, ou seja, possa ser alterado conforme necessário. É aí que as APIs de estado do Compose entram em jogo.

O Compose fornece tipos de observáveis chamados de MutableState, que são integrados ao tempo de execução. Podemos usá-los para criar variáveis de estado mutáveis. Quando ocorre uma alteração nesses estados, o Compose automaticamente notifica o event handler, o que resulta na atualização da interface de usuário.

val count = mutableStateOf(0)

Mas há um problema no código acima. Se a função composable for acionada novamente devido a uma alteração no estado, o valor de “count” será resetado a cada vez e nosso código não funcionará corretamente.

Para evitar esse comportamento, podemos utilizar a API Remember. Basta adicionar a função composable inline chamada “remember” e fazer um wrap da inicialização da variável, dessa forma ela será mantida ao longo das chamadas e não será resetada.

val count = remember { mutableStateOf(0) }

Aqui vai uma dica: você também pode usar a keyword “by” do Kotlin, que utiliza properties delegate. Isso evitará que você precise usar “.value” toda vez que quiser acessar o valor atual do mutableState.

val count = remember { mutableStateOf(0) }
//count.value

val count by remember { mutableStateOf(0) }
//count

Embora o “remember” seja capaz de reter o estado durante novas chamadas, ele não preserva o estado em caso de alterações de configuração, como girar o dispositivo ou alterar o tema. Para garantir que o estado seja mantido nessas situações, devemos usar a API “rememberSaveable”, que persiste o estado em um Bundle.

Além disso, o “rememberSaveable” é especialmente útil para reter o estado de itens que não estão diretamente envolvidos em uma composição. Por exemplo, podemos usar essa API para preservar o estado de um checkbox em uma listagem.

val count by rememberSaveable { mutableStateOf(0) }

Recomposition

Nos tópicos anteriores, falamos várias vezes sobre as “novas chamadas” das funções composable, que têm como objetivo alterar os componentes de acordo com mudanças de estado ou parâmetros. Esse processo de reexecução das funções é conhecido como recomposition.

O Jetpack Compose utiliza um loop de atualização da interface do usuário para manter a sincronia entre o estado e a exibição na tela. Quando ocorre um evento, o event handler é acionado, alterando o estado do aplicativo. Isso por sua vez provoca uma recomposição da UI, onde os componentes são atualizados com base no novo estado. Esse ciclo de atualização entre estado e UI é repetido continuamente, garantindo que a interface do usuário reflita sempre o estado atual do aplicativo.

O processo de recomposition é otimizado para executar apenas as partes afetadas pela alteração de estado. Ele realiza uma reexecução seletiva, identificando quais composable functions precisam ser atualizadas com base nas mudanças de estado. Para isso, o Compose precisa saber quais estados devem ser rastreados, ou seja, quais estados são relevantes para a atualização da UI. Dessa forma, o Compose garante um desempenho eficiente, evitando reexecuções desnecessárias e atualizando apenas as partes relevantes.

Mas como que o Compose faz esse rastreamento?

O Compose possui um sistema de rastreamento de estado chamado “Compose’s state tracking system”. Ele agenda recompositions para qualquer composable que está lendo um determinado estado, tornando o Compose granular e capaz de responder apenas às funções composable associadas a esse estado específico, em vez de toda a tela.

Bom, agora que você já sabe o que é uma recomposition, vamos levantar alguns pontos importantes:

  • É importante evitar o uso de variáveis externas mutáveis que possam afetar o funcionamento da função composable. Se uma função composable for chamada com o mesmo estado e parâmetros, ela deve sempre produzir o mesmo resultado, sem efeitos colaterais. Isso garante um comportamento consistente e previsível no aplicativo.
  • As composables functions podem ser executadas em qualquer ordem. O Compose possui um mecanismo inteligente que determina a prioridade dos elementos e os desenha primeiro. Isso garante que a interface de usuário seja renderizada de forma eficiente e que os elementos mais importantes sejam exibidos corretamente, independentemente da ordem em que as funções são chamadas.
  • As composable functions são executadas de forma paralela, o que traz uma grande vantagem em termos de desempenho de renderização, especialmente em dispositivos com processadores multi-core. Isso significa que o Compose pode aproveitar ao máximo os recursos de hardware disponíveis, acelerando o processo de renderização da interface de usuário e proporcionando uma experiência mais fluida para o usuário.
  • Recompositions são tratadas de forma otimizada. Quando um parâmetro é alterado em uma composable function, o Compose espera que a recomposição atual seja concluída antes que o parâmetro mude novamente. Caso o parâmetro mude antes da recomposição finalizar, o Compose cancela a recomposição em andamento e inicia uma nova recomposição com o novo parâmetro. Isso garante que a interface de usuário seja atualizada corretamente, evitando problemas de inconsistência ou comportamento indesejado.
  • Para garantir uma animação suave e sem interrupções, é importante criar funções rápidas no Jetpack Compose. Ao otimizar o código e minimizar o tempo de execução da função, é possível evitar a ocorrência de “dropped frames” e manter a animação fluida. Portanto, ao lidar com animações que requerem atualizações frequentes, certifique-se de criar funções eficientes para obter o melhor desempenho possível.

State Drive UI

Como mencionado anteriormente, o Jetpack Compose é um framework declarativo. Vamos considerar a situação em que queremos ocultar ou remover um texto da tela quando o estado de uma variável for igual a 0. Se estivéssemos usando XML, teríamos que manipular manualmente a visibilidade do botão ou remover a visualização do layout. No entanto, no Jetpack Compose, em vez de realizar essas manipulações manuais, descrevemos como a interface deve ser em cada estado.

Isso faz com que, durante a recomposition, o componente seja adicionado ou removido da composição automaticamente. No caso do nosso exemplo, quando o estado possuir o valor 0, o elemento de texto é removido da árvore de componentes. Esse comportamento pode ser visualizado no Layout Inspector do Android Studio.

Resumindo, caso o componente tenha sido chamado durante a composition ou recomposition, esse elemento é adicionado na árvore. Caso não tenha sido chamado, ele é removido. Essa abordagem torna o Compose mais flexível e permite uma melhor organização e controle da interface do usuário

Isso acontece não só para elementos de UI mas também para variáveis com o "remember". Se não chamada, seu espaço de memória é deletado.

Stateful composable x Stateless composable

Quando um composable function possui um estado interno, chamamos essa função de stateful composable. Geralmente esses composables tende a ser menos reutilizáveis e mais difícil de serem testados, já que dependem de um estado. Já composables que não possuem nenhum estado interno são chamados de stateless composables, que são mais fáceis de serem reutilizados e mais fácil de testar.

Uma boa prática é evitar a criação de stateful composables quando o estado não precisa ser mantido internamente. Em vez disso, é recomendado realizar o state hoisting, que é o processo de transformar stateful composables em stateless composables.

“State hoisting” is a pattern of moving state to a composable’s caller to make a composable stateless.”

O state hoisting envolve mover o estado necessário para o componente pai e passá-lo como parâmetro para o composable filho. Isso torna os composables mais flexíveis e reutilizáveis, pois não dependem mais de um estado interno específico. Em vez disso, eles recebem o estado como entrada, permitindo que sejam utilizados com diferentes estados em diferentes contextos.

Essa prática traz vários benefícios, como simplificação dos composables, uma melhor separação de responsabilidades e facilitação dos testes. Além disso, torna o código mais claro, uma vez que as dependências de estado são explicitamente definidas.

É muito comum que, ao passar esses estados para o chamador e ter um listener dentro da composable function interna, precisemos atualizar essa variável de alguma forma. No entanto, uma vez que passamos a variável para cima, perdemos o acesso direto a ela. Nesse caso, podemos utilizar o padrão conhecido como UDF (Unidirectional Data Flow), no qual utilizamos uma função lambda como parâmetro. Essa função será usada para responder ao evento e, quando disparada, a função chamadora realizará a alteração apropriada no valor. Dessa forma, mantemos a integridade do fluxo unidirecional dos dados.

Agora que você compreende as partes essenciais do Jetpack Compose, é hora de colocar em prática. O Google oferece diversos codelabs que detalham como construir layouts usando o Jetpack Compose. Aproveite essa oportunidade para aprofundar seus conhecimentos e experimentar na prática. Desejo a você boa sorte em sua jornada de aprendizado e desenvolvimento com o Jetpack Compose.

Referências

https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1

--

--

Daniely Murua
wearejaya

Mobile engineer at Jaya Tech and gaming enthusiast. 📱🎮