Aula 05: Layouts básicos com Jetpack Compose

Fundamentos de Layouts

Componentes Principais

Os componentes básicos de layout no Compose são Row, Column, e Box. Esses componentes permitem arranjar elementos na vertical, horizontal, ou de forma sobreposta.

518dbfad23ee1b05.png

A Column é um componente que alinha seus filhos verticalmente. Cada elemento filho é empilhado abaixo do anterior. É útil para criar layouts estruturados verticalmente, como listas ou seções de informações. Você pode ajustar o espaçamento entre os elementos e o alinhamento dentro da coluna para personalizar a aparência de acordo com as necessidades do seu design.

A Row é semelhante à Column, mas alinha seus filhos horizontalmente. Os elementos são posicionados lado a lado, permitindo a criação de layouts horizontais como fileiras de botões ou ícones. Assim como com Columns, você pode especificar o espaçamento e alinhamento dos elementos para controlar exatamente como eles são apresentados na tela.

O Box é um componente flexível que permite sobrepor elementos um sobre o outro. Ele funciona como um contêiner de layout onde você pode posicionar elementos em camadas. Isso é útil para criar designs complexos ou personalizados onde os elementos se sobrepõem parcialmente ou ficam centralizados dentro de uma área especificada.

@Composable
fun RowColumnExample() {
    Column {
        Text("Column Item 1")
        Text("Column Item 2")
        Row {
            Text("Row Item 1")
            Text("Row Item 2")
        }
    }
}

Modificadores

Modifiers, ou modificadores, são ferramentas no Jetpack Compose que permitem modificar ou estilizar os componentes. Eles podem ser usados para alterar propriedades como tamanho, padding, background, e até funcionalidades de clique. Modifiers são aplicados de maneira encadeada, proporcionando uma maneira flexível e declarativa de personalizar a UI.

import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding

@Composable
fun StyledText() {
    Text(
        text = "Styled with Modifiers",
        modifier = Modifier
            .background(Color.Cyan)
            .padding(16.dp)
            .width(200.dp)
            .height(50.dp)
    )
}

Modificadores fillMaxWidth e padding.

Modificadores podem ter sobrecargas, permitindo, por exemplo, especificar diferentes maneiras de criar padding.

Arrangement e Alignment

O Alignment define como os elementos são alinhados dentro de um contêiner, como Row, Column, ou Box. Ele determina a posição dos elementos no eixo vertical ou horizontal (eixo principal de cada elemento), oferecendo controle preciso sobre como os componentes são posicionados tanto na direção principal quanto na direção cruzada do layout.

O Arrangement controla o espaçamento entre os elementos filhos dentro de Rows ou Columns, utilizando o eixo secundário como referência. Ele permite especificar como os elementos devem ser distribuídos, seja com espaçamento entre eles, tudo no início, centro ou fim do layout. Isso facilita a criação de interfaces esteticamente agradáveis ao controlar como o espaço extra é utilizado.

@Composable
fun AlignedContent() {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text("Left")
        Text("Right")
    }
}

Box

O Box pode ser utilizado para sobrepor itens e criar designs complexos onde os elementos precisam estar um em cima do outro.

@Composable
fun BoxExample() {
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(Color.LightGray)
    ) {
        Text("Bottom", Modifier.align(Alignment.BottomStart))
        Text("Top", Modifier.align(Alignment.TopEnd))
    }
}

Lazy Layouts

Utilize LazyColumn e LazyRow para listas eficientes e pagináveis, que são essenciais para tratar grandes quantidades de dados sem comprometer a performance.

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items

@Composable
fun SimpleLazyColumn() {
    LazyColumn {
        items(listOf("Apple", "Banana", "Cherry")) { fruit ->
            Text("Fruit: $fruit", Modifier.padding(8.dp))
        }
    }
}

Customização e Temas

Themes

A customização de temas no Jetpack Compose permite adaptar a aparência do aplicativo em nível global. Utilizando o MaterialTheme, você pode definir cores, tipografia, e formas para fornecer uma identidade visual consistente em toda a aplicação. A personalização de temas oferece uma maneira eficiente de garantir uma experiência de usuário coerente e estilizada.

Abra o arquivo ui/theme/Theme.kt, você verá detalhes da implementação do tema da aplicação.

MaterialTheme é uma função composable que reflete os princípios de estilo da especificação do Material Design.

import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface

@Composable
fun ThemedGreeting() {
    MaterialTheme(
        colors = MaterialTheme.colors.copy(
            primary = Color.Magenta,
            primaryVariant = Color.DarkMagenta,
            secondary = Color.Cyan
        )
    ) {
        Surface {
            Text("Hello with Theme")
        }
    }
}

Para o uso de Ícones

Use o composable IconButton, por exemplo:

IconButton(onClick = onClose) {
    Icon(Icons.Filled.Close, contentDescription = "Close")
}

Adicione a seguinte linha às dependências no seu arquivo app/build.gradle.kts para mais ícones:


 implementation("androidx.compose.material:material-icons-extended") 

Práticas Avançadas

Criando Layouts Compostos

Layouts compostos personalizados que melhor se encaixam nas necessidades específicas do design do aplicativo.

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp

@Composable
fun CustomButton(content: @Composable () -> Unit) {
    Box(
        modifier = Modifier
            .padding(8.dp)
            .background(Color.Blue)
            .clickable { }
            .padding(16.dp)
    ) {
        content()
    }
}

@Composable
fun CustomButtonExample() {
    CustomButton {
        Text("Click Me!")
    }
}

Interoperabilidade com Views Tradicionais

É possível utilizar Views dentro do Compose e vice-versa, facilitando a migração de aplicativos existentes para o novo sistema.

// No arquivo MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AndroidViewExample()
        }
    }
}

@Composable
fun AndroidViewExample() {
    AndroidView(
        factory = { context ->
            Button(context).apply {
                text = "Old View Button"
                setOnClickListener {
                    // Código do botão ao ser clicado
                }
            }
        }
    )
}

note

  • Diferença entre componentes Surface e Scaffold:
    • São estruturas fundamentais de layout. Algo como containers;
    • Porém, Scaffold torna mais fácil o tratamento de área cobertas pelo Sistema Operacional (como barras de notificação, notch de câmera, botões de navegação, etc.);
    • Isso é ainda mais importante após a decisão do Google de que as aplicações devem usar enableEdgeToEdge().
      • Isso significa que a aplicação é que deve tomar conta dessas áreas cobertas, ocupando a tela de "borda à borda";
    • Com o Surface esse controle se torna bem mais complexo.

Referências


Exemplo Prático -> RGB Counter

https://github.com/tads-ufpr-alexkutzke/ds151-aula-05-counter

Desenvolvimento passo a passo (commits)

Cria Counter Composable

Diffs do commit


Estrutura básica para o contador

Diffs do commit

O modificador weight faz o elemento preencher todo o espaço disponível, afastando os outros elementos que não têm peso, chamados de inflexíveis.


Tentativa frustada de incrementar o contador

Diffs do commit

O motivo pelo qual a mutação dessa variável não aciona recomposições é que ela não está sendo rastreada pelo Compose. Além disso, cada vez que Greeting é chamado, a variável será redefinida para false.

Use o mutableStateOf.

No entanto, você não pode simplesmente atribuir mutableStateOf a uma variável dentro de um composable.

Para preservar o estado entre recomposições, lembre o estado mutável usando remember.


Primeiro contador funcional

Diffs do commit

Se o estado mudar, os composables que leem esses campos serão recompostos para exibir as atualizações.

Em aplicativos Android, há um loop principal de atualização da UI assim:

f415ca9336d83142.png

7d3509d136280b6c.png

Se a função é chamada durante a composição inicial ou em recomposições, dizemos que está presente na Composição.

Você pode inspecionar o layout do app gerado pelo Compose usando a ferramenta Layout Inspector do Android Studio,

Ferramenta Layout Inspector do Android Studio navegando para Tools > Layout Inspector. 1f8e05f6497ec35f.png


Correções de layout para apresentação no emulador

Diffs do commit


Botão de decremento

Diffs do commit

Vamos mudar nossa pré-visualização para emular a largura comum de um celular pequeno, 320dp. Adicione um parâmetro widthDp à anotação @Preview.


Melhora layout

Diffs do commit


Uso do by

Diffs do commit

Aqui usamos uma palavra-chave by ao invés do =. Isso é um delegate de propriedade que poupa você de digitar .value toda vez.


Adiciona parâmetros de mínimo e máximo

Diffs do commit


Adiciona CounterScreen com teste de vários contadores

Diffs do commit


Monta o layout de RGBCounterScreen

Diffs do commit


Inícia o State Hoisting do Counter

Diffs do commit

Em funções Composable, o estado que é lido ou modificado por várias funções deve existir em um ancestral comum — esse processo é chamado de State Hoisting, ou elevação de estado.

Como passamos eventos para cima? Passando callbacks para baixo. Callbacks são funções passadas como argumentos para outras funções e executadas quando o evento ocorre.

O padrão geral para elevação de estado no Jetpack Compose é substituir a variável de estado por dois parâmetros:

  • Valor: T - o valor atual a ser exibido;

  • OnValueChange: (T) -> Unit - um evento que solicita a mudança de valor com um novo valor T;

  • Propriedades importantes:

    • Fonte única de verdade: ao mover o estado em vez de duplicá-lo, estamos garantindo que exista apenas uma fonte única de verdade. Isso ajuda a evitar bugs.
    • Compartilhável: Estado elevado pode ser compartilhado com múltiplos composables.
    • Interceptável: Chamadores dos composables sem estado podem decidir ignorar ou modificar eventos antes de mudar o estado.
    • Desacoplado: O estado para uma função composable sem estado pode ser armazenado em qualquer lugar. Por exemplo, em um ViewModel.

Exemplo de uso do novo Stateless Counter

Diffs do commit

note

No Compose, você não esconde elementos da UI. Em vez disso, você simplesmente não os adiciona à composição, então eles não são incluídos na árvore de UI que o Compose gera. Você faz isso com simples lógica condicional em Kotlin.


Adicionar os limites ao formato Stateless

Diffs do commit


Versão funcional da RGBCounterScreen

Diffs do commit


Adiciona parâmetro step ao contador

Diffs do commit


Adiciona parâmetro text ao contador

Diffs do commit


Ideia simples para diminuir repetição de código

Diffs do commit


Atualiza mainactivity

Diffs do commit


important

Se você executar o aplicativo em um dispositivo, clicar nos botões e então girar, a tela será mostrada novamente. A função remember funciona apenas enquanto o composable for mantido na Composição. Quando você gira, toda a atividade é reiniciada, então todo o estado é perdido. Isso também acontece com qualquer mudança de configuração e com a morte do processo.

Na próxima aula veremos como resolver esse problema.