Jetpack Compose: o framework de UI do Android para os próximos 10 anos.

Nelson Glauber
8 min readOct 30, 2019

--

No Google I/O de 2019 o Google anunciou o Jetpack Compose, um toolkit desenvolvido com o intuito de simplificar a criação de interfaces gráficas no Android. Meses depois, no Android Dev Summit 2019, o Google o apresentou as novidades do Jetpack Compose e sua integração com o Android Studio 4.0.

O Compose utiliza Kotlin para implementar a abordagem de UI declarativa de composição de componentes, algo muito parecido com o React Native, Flutter e Swift UI, onde a hierarquia de componentes é redesenhada automaticamente quando o seu estado interno é modificado.

Em seu lançamento, foi bastante enfatizado que o Compose tende a ser a solução de UI do Android para os próximos anos.

Nesse post vou apresentar como dar os primeiros passos com o Jetpack Compose utilizando Android Studio, que pode ser baixado clicando aqui.

Criando o Projeto de exemplo

Inicie um novo projeto no Android Studio 4.2 e será exibida a tela a seguir:

Tela de novo projeto do Android Studio

Selecione a opção "Empty Compose Activity", clique em "Next" e em seguida preencha os demais campos para a criação do projeto.

Uma vez criado o projeto, no build.gradle do módulo app, temos a seção buildFeatures onde o compose é habilitado. E no bloco composeOptions, podemos observar algumas configurações adicionais para o compilador do Kotlin feitas exclusivamente para o Compose, como a versão do compilador do Kotlin que inclui as mudanças necessárias para o uso do Compose. Por fim, na seção dependencies temos as dependências das bibliotecas do Compose.

android {
...
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion '1.4.32'
}
}

dependencies {
...

implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
}

Abra a MainActivity e o código deve estar como a seguir:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Demo1Theme {
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
}
}

@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
Demo1Theme {
Greeting("Android")
}
}

Como é possível perceber, no método setContent está sendo passado um lambda onde a composição da UI inicia com um <SeuApp>Theme que trará as definições de estilo da UI. Dentro do tema, temos a declaração do primeiro componente customizado chamado Greeting onde é passada a mensagem que será exibida na tela.

Perceba que Greeting nada mais é do que uma função com a anotação @Composable onde temos a declaração do componente Text (que é similar ao bom e velho TextView) que faz parte da biblioteca core do Compose e exibe um texto na tela.

Mas o interessante desse exemplo é que podemos ter a pré-visualização do componente dentro do próprio Android Studio. Isso é feito utilizando a anotação @Preview na função DefaultPreview. Para isso, basta compilar a aplicação (menu Build > Make Project) e o resultado será como a seguir:

Visualizando o componente do Compose no Android Studio

Sempre que for feita uma mudança no código, é preciso pressionar o botão "Build & Refresh" como indicado na imagem para que o preview seja atualizado.

Rows and Columns

A maneira mais simples de adicionar componentes à tela é utilizando os componentes Row e Column. Ao adicionar componentes dentro de um Column eles ficam um abaixo do outro e no Row eles ficam um ao lado do outro.

@Composable
fun RowsAndColumns() {
Column {
Text("This is the first text")
Text("This is the second text")
Text("This is the third text")
Row {
Text("Col 1")
Text("Col 2")
Text("Col 3")
}
}
}

Substitua o componente Greeting por RowsAndColumns na MainActivity e na função DefaultPreview e o resultado ficará como a seguir:

Exemplo do uso dos layouts Column e Row

Diversos componentes do Compose possuem o parâmetro modifier que permite aplicar diversas mudanças de forma sucessiva e encadeada no componente, tais como: espaçamento, background, bordas, largura/altura, evento de clique, etc.

Vejamos o código a seguir:

@Composable
fun RowsAndColumns() {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("This is the first text")
Text("This is the second text")
Text("This is the third text")
Row(
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Text("Col 1")
Text("Col 2")
Text("Col 3")
}
}
}

O resultado ficará como a seguir:

Perceba que no componente Column, foi utilizado o parâmetro modifier para definir um espaçamento utilizando a função padding() (dp é uma extension function da classe Int) e a função fillMaxWidth() seria um equivalente ao velho "match_parent" do XML. Os itens são centralizados utilizando o parâmetro horizontalGravity.

Já na Row foi adicionado um espaçamento apenas no topo (padding com parâmetro top). Em seguida, para que os items fiquem distribuídos igualitariamente na horizontal, foi utilizado o valor Arrangement.SpaceEvenly.

State

Na abordagem atual de desenvolvimento de interfaces gráficas, o componente possui um estado interno, e sempre que os dados da aplicação são alterados, o componente de UI deve ser atualizado utilizando os dados da aplicação. O mesmo deve acontecer quando o componente visual muda seu estado, ele deve atualizar o modelo de dados da aplicação. Isso é uma fonte comum de bugs na aplicações, pois os dados e a UI podem ficar inconsistentes.

Umas das motivações do Compose é utilizar um "single source of truth", que basicamente significa que a fonte dos dados será única. Desta forma, os componentes de UI representarão seus estados utilizando os dados da própria aplicação, evitando assim divergências entre dados e a UI.

A maneira mais simples de adicionar um estado é utilizando a função mutableStateOf. A função remember permite lembrar de um determinado valor após o componente ser atualizado.

@Composable
fun DemoState() {
var isChecked by remember { mutableStateOf(false) }
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it}
)
}

A variável isChecked está sendo adicionada ao estado da tela. Perceba que o componente Checkbox confia nesse valor. Por isso, no onCheckedChange esse valor é atualizado, o que faz com que o componente seja redesenhado.

Mas uma tela normalmente possui um conjunto de estados e para representá-lo, temos duas opções:

// Opção 1
class Score(
homeTeam: String,
homeScore: Int,
visitorTeam: String,
visitorScore: Int
) {
var homeTeam by mutableStateOf(homeTeam)
var homeScore by mutableStateOf(homeScore)
var visitorTeam by mutableStateOf(visitorTeam)
var visitorScore by mutableStateOf(visitorScore)
}
// Opção 2
data class Score(
val homeTeam: String,
var homeScore: Int,
val visitorTeam: String,
var visitorScore: Int
)

A opção 1, apesar de ser mais verbosa e não permitir usar uma data class, possui a vantagem de permitir a atualização da UI simplesmente alterando a propriedade do objeto. Já a opção 2 é mais simples, pois usa uma data class normal, sem nenhuma mudança. Mas um ponto negativo é que para atualizar o estado é preciso fazer uma cópia do objeto original, alterar a(s) propriedade(s) desejada(s) e reatribuir o objeto. Outro problema é que se o objeto for passado para um nível muito profundo da hierarquia, o componente poderá não ser atualizado corretamente. Sendo assim, utilizarei a opção 1.

Criando uma tela com estado

Nesse exemplo será implementada uma tela que exibirá o placar de uma partida de futebol (mas poderia se vôlei, basquete, …), onde serão exibidos os nomes dos times e será possível incrementar e decrementar o placar da partida. Essa tela será representada pela composable function ScoreView listada a seguir:

@Composable
fun ScoreView(score: Score) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.padding(bottom = 16.dp)
) {
TeamScore(
team = score.homeTeam,
score = score.homeScore,
onUpdate = { newScore ->
score.homeScore = newScore
}
)
Text(
text = "x",
modifier = Modifier
.padding(start = 16.dp, end = 16.dp),
style = TextStyle(
fontSize = 24.sp,
color = Color.Red
)
)
TeamScore(
team = score.visitorTeam,
score = score.visitorScore,
onUpdate = { newScore ->
score.visitorScore = newScore
}
)
}
OutlinedButton(
onClick = {
score.homeScore = 0
score.visitorScore = 0
}
) {
Text("Reset")
}
}
}

Nesta função, o objeto Score é passado por parâmetro. Quando esse objeto for modificado, o componente será automaticamente atualizado.

O componente inicia com um Column que ocupará todo o tamanho da tela. Isso é feito utilizando a função fillMaxSize, que é o equivalente a fillMaxWidth e fillMaxHeight juntos. Os componentes filhos ficarão centralizados verticalmente (verticalArrangement) e horizontalmente (horizontalAlignment).

Em seguida, uma Row é utilizada para exibir o placar de cada time. O placar é representado pelo componente TeamScore que veremos logo a seguir.

Entre o placar de cada time é exibido um Text com um "x". Perceba que foi configurado o estilo do texto por meio do parâmetro style. No objeto TextStyle, foi informado o tamanho do texto e a cor (fontSize e color).

Por fim, temos um OutlinedButton que exibirá o botão com uma borda e que reiniciará o placar do jogo.

Vejamos agora o componente TeamScore:

@Composable
fun TeamScore(
team: String,
score: Int,
onUpdate: (Int) -> Unit
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = team,
style = TextStyle(
fontSize = 24.sp
),
modifier = Modifier.padding(bottom = 16.dp)
)
Button(
onClick = {
onUpdate(score + 1)
}
) { Text("+") }
Text(
text = score.toString(),
style = MaterialTheme.typography.h4,
modifier = Modifier.padding(16.dp)
)
Button(
onClick = {
onUpdate(max(score - 1, 0))
}
) { Text("-") }
}
}

Este componente recebe como parâmetro: o nome do time, o placar atual do time, um lambda que será executado para informar o novo valor do placar.

A UI é composta de um Column que exibe os componentes de forma centralizada. Dentro dele, temos: um Text que exibe o nome do time; dois componentes Button que possuem um estilo diferente do botão de “Reset” e que chamam a função onUpdate (um incrementando, outro decrementando).

Perceba que o Text que exibe o score está usando o style para definir o tamanho do texto por meio da sintaxe MaterialTheme.typography.h4 onde é o utilizado o tema corrente da aplicação (MaterialDesign) para obter o estilo de texto h4 (header 4, da mesma forma do HTML).

Finalmente, utilize o componente ScoreView na MainActivity.

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Demo1Theme {
ScoreView(
Score("Corinthians", 0, "São Paulo", 0)
)
}
}
}
}

O resultado ficará como a seguir:

Aplicação de placar escrita com Jetpack Compose

Conclusão

O Compose traz para a plataforma Android os mesmos conceitos de frameworks de UI modernos e amplamente utilizados no mercado como React, Flutter, SwiftUI, etc. Apesar de ainda se encontrar em um estágio inicial, o Google parece estar apostando bastante Compose, e a ideia é que ele seja a nova forma padrão de construção de interfaces gráficas no Android.

A previsão de lançamento do Jetpack Compose é 2021, mas cabe a nós desenvolvedores ir se adaptando a essa nova forma de escrever UI no Android.

No momento, o Compose não está pronto para produção, e vem tendo constantes mudanças de API (esse artigo, inclusive já foi atualizado duas vezes), mas nós como desenvolvedores já devemos começar a estudar, nos preparar e se acostumar com essa API de modo a estarmos prontos para quando sua versão estável estiver disponível.

Espero que tenham gostado do post. Aproveite para ler a parte II ;)

4br4ç05
nglauber

--

--

Nelson Glauber

Android Developer. Google Developer Expert for Android/Kotlin. Author of “Dominando o Android”.