Noções básicas do Jetpack Compose

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

1. Antes de começar

Jetpack Compose é um kit de ferramentas moderno criado para simplificar o desenvolvimento de IUs. Ele combina um modelo de programação reativa com a concisão e a facilidade de uso da linguagem de programação Kotlin. Ele é totalmente declarativo, ou seja, você descreve a IU chamando uma série de funções que transformam dados em uma hierarquia de IUs. Quando os dados subjacentes mudam, o framework reexecuta automaticamente essas funções, atualizando a hierarquia de IUs.

Um app Compose é formado por funções de composição: apenas funções regulares marcadas com @Composable, que podem chamar outras funções de composição. Basta usar uma função para criar um novo componente de IU. A anotação instrui o Compose a adicionar suporte especial à função para atualizar e manter a IU ao longo do tempo. O Compose permite estruturar o código em pequenos blocos. As funções de composição costumam ser chamadas de "composições" para abreviar.

Ao criar pequenas composições reutilizáveis, é fácil criar uma biblioteca de elementos de IU usados no app. Cada um é responsável por uma parte da tela e pode ser editado de forma independente.

Para receber mais suporte durante este codelab, confira as orientações neste vídeo (em inglês):

Pré-requisitos

  • Experiência com a sintaxe do Kotlin, incluindo lambdas.

O que você vai fazer

Neste codelab, você vai aprender:

  • O que é o Compose
  • Como criar IUs com o Compose
  • Como gerenciar o estado em funções de composição
  • Como criar uma lista de desempenho
  • Como adicionar animações
  • Como definir o estilo e o tema de um app

Você vai criar um app com uma tela de integração e uma lista de itens com animações de abertura:

5dcc23167391e246.gif

O que é necessário

2. Como iniciar um novo projeto do Compose

Para iniciar um novo projeto do Compose, abra o Android Studio e selecione Start a new Android Studio project, conforme mostrado:

f5980dbff6f0fb7c.jpeg

Caso a tela acima não seja mostrada, acesse File > New > New Project.

Ao criar um novo projeto, selecione a opção Empty Compose Activity nos modelos disponíveis.

a67ba73a4f06b7ac.png

Clique em Next e configure o projeto normalmente, chamando-o de "Basics Codelab". Selecione uma minimumSdkVersion com API de nível 21 ou mais recente. Esse é o nível mínimo de API com suporte no Compose.

Ao selecionar o modelo Empty Compose Activity, o seguinte código será gerado no seu projeto:

  • O projeto já está configurado para usar o Compose.
  • O arquivo AndroidManifest.xml foi criado.
  • Os arquivos build.gradle e app/build.gradle contêm opções e dependências necessárias para o Compose.

Depois de sincronizar o projeto, abra MainActivity.kt e confira o código.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

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

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

Na próxima seção, você vai aprender sobre o que cada método faz e como é possível melhorá-los para criar layouts flexíveis e reutilizáveis.

Solução para o codelab

O código da solução deste codelab está disponível no GitHub:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Se preferir, faça o download do repositório como um arquivo ZIP:

O código da solução está disponível no projeto BasicsCodelab. Recomendamos que você siga o codelab passo a passo em seu próprio ritmo e, se necessário, consulte a solução. Durante o codelab, você verá snippets de código que precisam ser adicionados ao projeto.

3. Primeiros passos com o Compose

Analise as classes e os métodos diferentes relacionados ao Compose que foram gerados pelo Android Studio.

Funções de composição

Uma função de composição é uma função regular anotada com @Composable. Isso permite que a função chame outras funções @Composable dentro dela. É possível ver como a função Greeting é marcada como @Composable. Essa função produzirá uma parte da hierarquia de IUs que exibe a entrada fornecida, String. Text é uma função de composição fornecida pela biblioteca.

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

Compose em um app Android

Com o Compose, as Activities continuam sendo o ponto de entrada para um app Android. No nosso projeto, a MainActivity é iniciada quando o usuário abre o app, conforme especificado no arquivo AndroidManifest.xml. Use setContent para definir o layout, mas, em vez de usar um arquivo XML, como você faria no sistema de visualização tradicional, chame funções de composição.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme é uma maneira de definir o estilo de funções de composição. Saiba mais sobre isso na seção Como aplicar temas ao app. Para ver como o texto aparece na tela, execute o app em um emulador ou dispositivo ou use a visualização do Android Studio.

Para usar a visualização do Android Studio, basta marcar qualquer função de composição sem parâmetros ou funções com parâmetros padrão com a anotação @Preview e criar seu projeto. Você já pode ver uma função Preview Composable no arquivo MainActivity.kt. É possível ter várias visualizações no mesmo arquivo e atribuir nomes a elas.

@Preview(showBackground = true, name = "Text preview")
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

debde226026ae047.png

A visualização talvez não apareça se a opção Code f66a8adcef249de5.png estiver selecionada. Clique em Split f3c0e2f3221dadcb.png para abrir a visualização.

4. Como ajustar a IU

Vamos começar definindo uma cor de segundo plano diferente para Greeting. Para isso, envolva o Text de composição com uma Surface. Como a Surface usa uma cor, use MaterialTheme.colors.primary.

@Composable
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.primary) {
        Text (text = "Hello $name!")
    }
}

Os componentes aninhados em Surface serão desenhados sobre a cor do segundo plano.

Ao adicionar esse código ao projeto, você verá um botão Build & Refresh no canto superior direito do Android Studio. Toque nele ou crie o projeto para conferir as novas mudanças na visualização.

9632f3ca76cbe115.png

Confira as novas mudanças na visualização:

8216bdbc85a6ba94.png

Talvez você não tenha observado um detalhe importante: o texto agora está em branco. Quando definimos isso?

Não foi você! Os componentes do Material Design, como androidx.compose.material.Surface, são criados para melhorar sua experiência ao cuidar dos recursos comuns que você provavelmente quer usar no app, como escolher uma cor adequada para o texto. Dizemos que o Material Design tem opinião porque fornece bons padrões comuns à maioria dos apps. Os componentes do Material Design no Compose são criados com base em outros componentes básicos (em androidx.compose.foundation), que também podem ser acessados nos componentes do app caso você precise de mais flexibilidade.

Nesse caso, a Surface entende que, quando a cor do segundo plano é definida como primary, qualquer texto nela precisa usar a cor onPrimary, que também é definida no tema. Saiba mais sobre isso na seção Como aplicar temas no app.

Modificadores

A maioria dos elementos de IU do Compose, como Surface e Text, aceita um parâmetro modifier opcional. Os modificadores informam para um elemento da IU como serão dispostos, exibidos ou se comportarão no layout pai.

Por exemplo, o modificador padding aplicará um pouco de espaço ao redor do elemento que ele decora. Você pode criar um modificador de padding com Modifier.padding().

Agora, adicione padding ao Text na tela:

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

@Composable
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.primary) {
        Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
    }
}

Clique em Build & Refresh para conferir as novas mudanças.

4241a60d72a08f0b.png

Existem dezenas de modificadores que podem ser usados para alinhar, animar, dispor, tornar clicáveis ou roláveis, transformar etc. Para saber mais, consulte a Lista de modificadores do Compose. Você usará alguns deles nas próximas etapas.

5. Como reutilizar composições

Quanto mais componentes adicionar à IU, mais níveis de aninhamento você criará. Isso pode afetar a legibilidade se uma função se tornar muito grande. Ao criar pequenos componentes reutilizáveis, é fácil criar uma biblioteca de elementos de IU usados no app. Cada um é responsável por uma parte da tela e pode ser editado de forma independente.

Crie um elemento de composição chamado MyApp que inclua a saudação.

@Composable
private fun MyApp() {
    Surface(color = MaterialTheme.colors.background) {
        Greeting("Android")
    }
}

Isso permite limpar o callback onCreate e a visualização, porque é possível reutilizar a composição MyApp, evitando a duplicação de código. O arquivo MainActivity.kt ficará assim:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basicstep1.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp()
            }
        }
    }
}

@Composable
private fun MyApp() {
    Surface(color = MaterialTheme.colors.background) {
        Greeting("Android")
    }
}

@Composable
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.primary) {
        Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))
    }
}

@Preview(showBackground = true)
@Composable
private fun DefaultPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

6. Como criar colunas e linhas

Os três elementos básicos de layout padrão do Compose são Column, Row e Box.

518dbfad23ee1b05.png

Essas são funções de composição, ou seja, você pode colocar itens nelas. Por exemplo, cada filho dentro de uma Column será colocado na vertical.

// Don't copy over
Column {
    Text("First row")
    Text("Second row")
}

Agora, tente mudar Greeting para que ela mostre uma coluna com dois elementos de texto, como no exemplo abaixo:

e42bf870995a84d2.png

Talvez seja necessário mover o padding.

Compare seu resultado com esta solução:

import androidx.compose.foundation.layout.Column
...

@Composable
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.primary) {
        Column(modifier = Modifier.padding(24.dp)) {
            Text(text = "Hello,")
            Text(text = name)
        }
    }
}

Compose e Kotlin

As funções de composição podem ser usadas como qualquer outra função no Kotlin. Isso torna a criação de IUs muito eficiente, já que é possível adicionar instruções para influenciar como a IU será exibida.

Por exemplo, é possível usar uma repetição for para adicionar elementos à Column:

@Composable
fun MyApp(names: List<String> = listOf("World", "Compose")) {
    Column {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

b6265492ef236d70.png

Você ainda não definiu dimensões ou não adicionou restrições ao tamanho das composições. Portanto, cada linha ocupa o mínimo de espaço possível, e a visualização faz o mesmo. Vamos mudar a visualização para emular a largura comum de um smartphone pequeno de 320 dp. Adicione um parâmetro widthDp à anotação @Preview desta maneira:

@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

8722ec524e694ba5.png

Como os modificadores são muito usados no Compose, vamos praticar com um exercício mais avançado: tentar replicar o layout abaixo usando os modificadores fillMaxWidth e padding.

fd7cb2daa600875.png

Agora, compare o código com a solução:

@Composable
fun MyApp(names: List<String> = listOf("World", "Compose")) {
    Column(modifier = Modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String) {
    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
            Text(text = "Hello, ")
            Text(text = name)
        }
    }
}

Algumas considerações:

  • Os modificadores podem ter sobrecargas. Por exemplo, é possível especificar formas diferentes de criar um padding.
  • Para adicionar vários modificadores a um elemento, basta encadeá-los.

Há várias maneiras de alcançar esse resultado. Sendo assim, caso seu código não corresponda a esse snippet, isso não significa que ele está errado. No entanto, copie e cole esse código para continuar com o codelab.

Como adicionar um botão

Na próxima etapa, você vai adicionar um elemento clicável que abre a Greeting. Portanto, precisamos adicionar esse botão primeiro. O objetivo é criar o layout abaixo:

e74f07b36865a878.png

Button é uma composição fornecida pelo pacote do Material Design, que usa uma composição como o último argumento. Como os lambdas finais podem ser movidos para fora dos parênteses, você pode adicionar qualquer conteúdo ao botão como filho. Por exemplo, Text:

// Don't copy yet
Button(
    onClick = { } // You'll learn about this callback later
) {
    Text("Show less")
}

Para fazer isso, você precisa aprender a posicionar uma composição no final de uma linha. Como não há um modificador alignEnd, você define um weight para a composição no início. O modificador weight faz com que o elemento preencha todo o espaço disponível, tornando-o flexível, eliminando efetivamente os outros elementos que não têm ponderação, que são chamados de inflexíveis. Isso também torna o modificador fillMaxWidth redundante.

Agora tente adicionar o botão e colocá-lo como mostrado na imagem anterior.

Confira a solução aqui:

import androidx.compose.material.Button
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
...

@Composable
private fun Greeting(name: String) {

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { /* TODO */ }
            ) {
                Text("Show more")
            }
        }
    }
}

7. Estado no Compose

Nesta seção, você vai adicionar interação à tela. Até aqui, você criou layouts estáticos, mas agora vai fazer com que eles reajam às mudanças do usuário:

ae3c993d793aa843.gif

Antes de descobrir como tornar um botão clicável e como redimensionar um item, é necessário armazenar um valor que indique se cada item está aberto ou não, como o estado do item. Como precisamos ter um desses valores por saudação, o local lógico para isso é na composição Greeting. Veja esse booleano expanded e como ele é usado no código:

// Don't copy over
@Composable
private fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

Adicionamos também uma ação onClick e um texto de botão dinâmico. Veremos mais sobre isso posteriormente.

No entanto, isso não funcionará como esperado. Definir um valor diferente para a variável expanded não fará com que o Compose a detecte como uma mudança de estado. Portanto, nada acontecerá.

A modificação dessa variável não aciona recomposições porque ela não está sendo acompanhada pelo Compose. Além disso, sempre que Greeting for chamada, a variável será redefinida como falsa.

Para adicionar um estado interno a uma composição, use a função mutableStateOf, que faz com que o Compose recomponha funções que leiam esse State.

import androidx.compose.runtime.mutableStateOf
...

// Don't copy over
@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) // Don't do this!
}

No entanto, não é possível atribuir mutableStateOf a uma variável dentro de uma composição. Como explicado anteriormente, a recomposição pode ocorrer a qualquer momento em que a composição é chamada novamente, redefinindo o estado para um novo estado mutável com um valor de false.

Para preservar o estado nas recomposições, lembre-se do estado mutável usando remember.

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
...

@Composable
fun Greeting() {
    val expanded = remember { mutableStateOf(false) }
    ...
}

remember é usado para proteger contra a recomposição, para que o estado não seja redefinido.

Se você chamar a mesma composição em diferentes partes da tela, criará elementos de IU diferentes, cada um com sua própria versão do estado. Pense no estado interno como uma variável particular em uma classe.

A função de composição será automaticamente "assinada" para o estado. Se o estado mudar, as composições que lerem esses campos serão recompostas para exibir as atualizações.

Como mudar o estado e reagir às mudanças de estado

Para mudar o estado, você pode ter notado que Button tem um parâmetro chamado onClick, mas não aceita um valor. Em vez disso, ele usa uma função.

Você pode definir a ação a ser tomada ao clicar atribuindo uma expressão lambda a ela. Por exemplo, vamos alternar o valor do estado expandido e mostrar um texto diferente, dependendo do valor.

            OutlinedButton(
                onClick = { expanded.value = !expanded.value },
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }

Se você executar o app em um emulador, verá que, quando o botão é clicado, o estado expanded é alternado, acionando uma recomposição do texto dentro do botão. Cada Greeting mantém o próprio estado expandido porque pertence a diferentes elementos da IU.

825dd6d6f98bff05.gif

Codifique até este ponto:

@Composable
private fun Greeting(name: String) {
    val expanded = remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Como abrir o item

Agora vamos expandir um item quando solicitado. Adicione outra variável que dependa do nosso estado:

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
...

Você não precisa se lembrar de extraPadding em relação à recomposição porque ela depende de um estado e está fazendo um cálculo simples.

Agora podemos aplicar um novo modificador de padding à coluna:

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Se você executar em um emulador, vai notar que cada item pode ser aberto de maneira independente:

ae3c993d793aa843.gif

8. Elevação de estado

Nas funções de composição, o estado lido ou modificado por várias funções precisa estar em um ancestral comum. Esse processo é chamado de elevação de estado. Elevar significa levantar ou aumentar.

A elevação de estado evita a duplicação do estado e a introdução de bugs, ajuda a reutilizar as composições e facilita muito o teste delas. Por outro lado, o estado que não precisa ser controlado pelo pai de uma composição não deve ser elevado. A fonte da verdade pertence a quem cria e controla esse estado.

Por exemplo, vamos criar uma tela de integração para nosso app.

8c0da5d9a631ba97.png

Adicione o código a seguir a MainActivity.kt:

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment

...

@Composable
fun OnboardingScreen() {
    // TODO: This state should be hoisted
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = { shouldShowOnboarding = false }
            ) {
                Text("Continue")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen()
    }
}

Esse código contém muitos recursos novos:

  • Você adicionou uma nova composição chamada OnboardingScreen e uma nova visualização. Se você criar o projeto, perceberá que pode ter várias visualizações ao mesmo tempo. Também adicionamos uma altura fixa para verificar se o conteúdo está alinhado corretamente.
  • A Column pode ser configurada para exibir o conteúdo no centro da tela.
  • shouldShowOnboarding está usando uma palavra-chave by em vez de =. É um delegado de propriedade que evita que você digite .value todas as vezes.
  • Quando o botão é clicado, shouldShowOnboarding é definido como false, mas você ainda não está lendo o estado de nenhum lugar.

Agora podemos adicionar essa nova tela de integração ao nosso app. Queremos exibi-la na inicialização e depois ocultá-la quando o usuário pressionar "Continue".

No Compose, você não oculta elementos de IU. Em vez disso, você simplesmente não os adiciona à composição. Portanto, eles não são adicionados à árvore da IU gerada pelo Compose. Para isso, use uma lógica condicional de Kotlin simples. Por exemplo, para mostrar a tela de integração ou a lista de saudações, você pode fazer algo como:

// Don't copy yet
@Composable
fun MyApp() {
    if (shouldShowOnboarding) { // Where does this come from?
        OnboardingScreen()
    } else {
        Greetings()
    }
}

No entanto, não temos acesso a shouldShowOnboarding. É claro que precisamos compartilhar o estado que criamos em OnboardingScreen com a composição MyApp.

Em vez de compartilhar o valor do estado com o pai, elevamos o estado. Basta movê-lo para o ancestral comum que precisa acessá-lo.

Primeiro, mova o conteúdo de MyApp para uma nova composição chamada Greetings:

@Composable
fun MyApp() {
     Greetings()
}

@Composable
private fun Greetings(names: List<String> = listOf("World", "Compose")) {
    Column(modifier = Modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

Agora adicione a lógica para mostrar as diferentes telas no MyApp e elevar o estado.

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    if (shouldShowOnboarding) {
        OnboardingScreen(/* TODO */)
    } else {
        Greetings()
    }
}

Também precisamos compartilhar shouldShowOnboarding com a tela de integração, mas não vamos transmiti-lo diretamente. Em vez de permitir que OnboardingScreen mude nosso estado, é melhor nos avisar quando o usuário clicar no botão Continue.

Como transmitimos eventos? Transmitindo callbacks. Callbacks são funções transmitidas como argumentos para outras funções e executadas quando o evento ocorre.

Tente adicionar um parâmetro de função à tela de integração definida como onContinueClicked: () -> Unit para que você possa modificar o estado da MyApp.

Solução:

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    if (shouldShowOnboarding) {
        OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
    } else {
        Greetings()
    }
}

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier
                    .padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

Ao transmitir uma função e não um estado para OnboardingScreen, estamos tornando essa composição mais reutilizável e protegendo o estado contra mutação por outras composições. Em geral, é simples. Um bom exemplo é como a visualização de integração precisa ser modificada para chamar OnboardingScreen agora:

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {}) // Do nothing on click.
    }
}

Atribuir onContinueClicked a uma expressão lambda vazia significa "não fazer nada", o que é perfeito para uma visualização.

O app está bem parecido com um app real. Bom trabalho!

c8c6c011ec37fe84.gif

Código completo até agora:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp()
            }
        }
    }
}

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    if (shouldShowOnboarding) {
        OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
    } else {
        Greetings()
    }
}

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

@Composable
private fun Greetings(names: List<String> = listOf("World", "Compose")) {
    Column(modifier = Modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

9. Como criar uma lista de desempenho lento

Agora vamos deixar a lista de nomes mais realista. Até agora, você exibiu duas saudações em uma Column. No entanto, ela consegue lidar com milhares de saudações?

Mude o valor da lista padrão nos parâmetros de Greetings para usar outro construtor de lista que permita definir o tamanho da lista e preenchê-la com o valor contido no lambda (aqui, $it representa o índice da lista):

names: List<String> = List(1000) { "$it" }

Isso cria 1.000 saudações, mesmo as que não cabem na tela. Obviamente, isso não é um bom desempenho. Você pode tentar executá-lo em um emulador. Aviso: esse código pode congelar o emulador.

Para exibir uma coluna rolável, usamos uma LazyColumn. LazyColumn renderiza somente os itens visíveis na tela, permitindo ganhos de desempenho ao renderizar uma lista grande.

No uso básico, a API LazyColumn fornece um elemento items no escopo, em que a lógica de renderização de itens individuais é escrita:

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

@Composable
private fun Greetings(names: List<String> = List(1000) { "$it" } ) {
    LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

b9ffef51a5fbc8ca.gif

10. Estado persistente

Nosso app tem um problema: se você executá-lo em um dispositivo, clicar nos botões e girar, a tela de integração será exibida novamente. A função remember funciona somente enquanto a composição for mantida. Quando você faz a rotação, toda a atividade é reiniciada para que o estado seja perdido. Isso também acontece com qualquer mudança de configuração e após a interrupção do processo.

Em vez de usar remember, use rememberSaveable. Isso salvará cada estado que sobreviveu a mudanças de configuração (como rotações) e à interrupção do processo.

Agora substitua o uso de remember em shouldShowOnboarding por rememberSaveable:

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

Execute, gire, mude para o modo escuro ou elimine o processo. A tela de integração não é mostrada, a menos que você tenha fechado o app antes.

d7802a1acb90beba.gif

Demonstração de como uma mudança de configuração (mudando para o modo escuro) não mostra a integração novamente.

Com cerca de 120 linhas de código até agora, você conseguiu exibir uma lista de rolagem longa e de alto desempenho dos itens, cada um mantendo seu próprio estado. Além disso, como você pode ver, o app tem um modo escuro perfeitamente correto sem linhas extras de código. Você vai aprender sobre a aplicação de temas nas próximas unidades.

11. Animar a lista

No Compose, existem várias maneiras de animar a IU: de APIs de alto nível para animações simples a métodos de baixo nível para controle total e transições complexas. Leia sobre elas na documentação.

Nesta seção, você usará uma das APIs de baixo nível, mas não se preocupe, porque elas também podem ser muito simples. Vamos animar a mudança no tamanho que já implementamos:

83bbc35a3bd4b1b2.gif

Para isso, você vai usar a composição animateDpAsState. Ela retorna um objeto State cujo value será atualizado continuamente pela animação até que ela termine. É necessário um "valor de destino" com o tipo Dp.

Crie um extraPadding animado que dependa do estado expandido. Além disso, vamos usar o delegado da propriedade (a palavra-chave by):

@Composable
private fun Greeting(name: String) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

Execute o app e teste a animação.

animateDpAsState usa um parâmetro animationSpec opcional que permite personalizar a animação. Vamos fazer algo mais divertido, como adicionar uma animação com molas:

@Composable
private fun Greeting(name: String) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Surface(
    ...
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))

    ...

    )
}

Também vamos garantir que o padding nunca seja negativo. Caso contrário, ele poderá causar falhas no app. Isso introduz um bug de animação sutil que corrigiremos posteriormente em Toques finais.

A especificação spring não aceita parâmetros relacionados ao tempo. Em vez disso, as propriedades físicas (amortecimento e rigidez) dependem deles para tornar as animações mais naturais. Execute o app agora para testar a nova animação:

c14f0b8f617d21eb.gif

Nenhuma animação criada com animate*AsState pode ser interrompida. Isso significa que, se o valor de destino mudar no meio da animação, o animate*AsState reiniciará a animação e apontará para o novo valor. As interrupções parecem especialmente naturais com animações com molas:

f72863865f685a62.gif

Se você quiser analisar os diferentes tipos de animação, experimente usar outros parâmetros para spring, especificações diferentes (tween, repeatable) e mais funções: animateColorAsState ou um tipo diferente de animação da API Animation.

Código completo desta seção

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp()
            }
        }
    }
}

@Composable
fun MyApp() {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    if (shouldShowOnboarding) {
        OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
    } else {
        Greetings()
    }
}

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

@Composable
private fun Greetings(names: List<String> = List(1000) { "$it" } ) {
    LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
private fun Greeting(name: String) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

12. Como definir o estilo e aplicar temas no app

Você não definiu o estilo de nenhuma composição até o momento, mas já conseguiu um padrão decente, incluindo o suporte ao modo escuro. Vamos ver o que são BasicsCodelabTheme e MaterialTheme.

Se você abrir o arquivo ui/Theme.kt, verá que BasicsCodelabTheme usa MaterialTheme na implementação:

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        content = content
    )
}

MaterialTheme é uma função de composição que reflete os princípios de estilo da especificação do Material Design. Essas informações de estilo são aplicadas em cascata aos componentes que estão dentro do content, que pode ler as informações para definir o estilo. Na IU, você já está usando BasicsCodelabTheme da seguinte maneira:

    BasicsCodelabTheme {
        MyApp()
    }

Como BasicsCodelabTheme envolve MaterialTheme internamente, MyApp é estilizado com as propriedades definidas no tema. Em qualquer composição descendente, é possível recuperar três propriedades de MaterialTheme: colors, typography e shapes. Use-as para definir o estilo de cabeçalho como um dos Texts:

            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.h4)
            }

A composição Text no exemplo acima define um novo TextStyle. Você pode criar seu próprio TextStyle ou recuperar um estilo definido pelo tema usando MaterialTheme.typography, que é preferencial. Essa construção oferece acesso aos estilos de texto definidos pelo Material Design, como h1h6, body1,body2, caption, subtitle1 etc. No seu exemplo, você usa o estilo h4 definido no tema.

Crie o app agora para conferir nosso texto com estilo recém-definido:

471658bc17da5b67.png

Em geral, é muito melhor manter as cores, as formas e os estilos de fonte em um MaterialTheme. Por exemplo, o modo escuro seria difícil de implementar se você codificasse cores e exigiria muito trabalho propenso a erros para corrigir.

No entanto, às vezes, é necessário desviar um pouco da seleção de cores e estilos de fonte. Nessas situações, é melhor basear a cor ou o estilo em uma cor ou um estilo existente.

Para isso, é possível modificar um estilo predefinido usando a função copy. Coloque o número em negrito:

                Text(
                    text = name,
                    style = MaterialTheme.typography.h4.copy(
                        fontWeight = FontWeight.ExtraBold
                    )
                )

Dessa forma, se você precisar mudar a família de fontes ou qualquer outro atributo de h4, não terá que se preocupar com os pequenos desvios.

Este vai ser o resultado na janela de visualização:

3c9a6d5d0939c813.png

Ajustar o tema do app

Tudo o que está relacionado ao tema atual pode ser encontrado nos arquivos dentro da pasta ui. Por exemplo, as cores padrão que estamos usando até agora são definidas em Color.kt.

Vamos começar definindo novas cores. Adicione-as a Color.kt:

val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)

Agora atribua-as à paleta do MaterialTheme em Theme.kt:

private val LightColorPalette = lightColors(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

Se você voltar para MainActivity.kt e atualizar a visualização, vai encontrar as novas cores:

358b92c429c4c579.png

No entanto, você ainda não modificou as cores escuras. Antes de fazer isso, vamos configurar as visualizações. Adicione uma anotação @Preview extra a DefaultPreview com UI_MODE_NIGHT_YES:

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "DefaultPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

Isso adiciona uma visualização no modo escuro.

7cfcdecdeccaf627.png

Em Theme.kt, defina a paleta para cores escuras:

private val DarkColorPalette = darkColors(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

Agora nosso app tem temas e estilos.

351d2a0ff94056d1.png

Código final para Theme.kt

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable

private val DarkColorPalette = darkColors(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

private val LightColorPalette = lightColors(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        content = content
    )
}

13. Toques finais!

Nesta etapa, você aplicará o que já sabe e aprenderá novos conceitos com apenas algumas dicas. Você vai criar o seguinte:

5dcc23167391e246.gif

Substituir o botão por um ícone

  • Use a composição IconButton com um filho Icon.
  • Use Icons.Filled.ExpandLess e Icons.Filled.ExpandMore, que estão disponíveis no artefato material-icons-extended. Adicione a seguinte linha às dependências no arquivo app/build.gradle:
implementation "androidx.compose.material:material-icons-extended:$compose_version"
  • Modifique os paddings para corrigir o alinhamento.
  • Adicione uma descrição do conteúdo para acessibilidade (consulte "Usar recursos de string" abaixo).

Usar recursos de string

A descrição do conteúdo de "Mostrar mais" e "Mostrar menos" precisa estar presente, e é possível adicioná-la com uma simples instrução if:

contentDescription = if (expanded) "Show less" else "Show more"

No entanto, strings codificadas não são uma prática recomendada. Você pode usar as strings no arquivo strings.xml.

Você pode usar a opção "Extract string resource" em cada string, disponível em "Context Actions" no Android Studio, para fazer isso automaticamente.

Como alternativa, abra app/src/res/values/strings.xml e adicione os seguintes recursos:

<string name="show_less">Show less</string>
<string name="show_more">Show more</string>

Mostrar mais

O texto "Composem ipsum" aparece e desaparece, acionando uma mudança no tamanho de cada card.

  • Adicione um novo Text à coluna dentro de Greeting que será exibida quando o item for expandido.
  • Remova o extraPadding e aplique o modificador animateContentSize à Row. Isso automatizará o processo de criação da animação, o que seria difícil de fazer manualmente. Além disso, não é mais necessário usar coerceAtLeast.

Adicionar elevação e formas

  • Você pode usar o modificador shadow com o modificador clip para ter a aparência do card. No entanto, há uma composição do Material Design que faz exatamente isso: Card.

Código final

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.R
import com.codelab.basics.ui.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp()
            }
        }
    }
}

@Composable
private fun MyApp() {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    if (shouldShowOnboarding) {
        OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
    } else {
        Greetings()
    }
}

@Composable
private fun OnboardingScreen(onContinueClicked: () -> Unit) {
    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

@Composable
private fun Greetings(names: List<String> = List(1000) { "$it" } ) {
    LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String) {
    Card(
        backgroundColor = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by remember { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .padding(12.dp)
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(12.dp)
        ) {
            Text(text = "Hello, ")
            Text(
                text = name,
                style = MaterialTheme.typography.h4.copy(
                    fontWeight = FontWeight.ExtraBold
                )
            )
            if (expanded) {
                Text(
                    text = ("Composem ipsum color sit lazy, " +
                        "padding theme elit, sed do bouncy. ").repeat(4),
                )
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }

            )
        }
    }
}

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "DefaultPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

14. Parabéns

Parabéns! Você aprendeu as noções básicas do Compose.

Solução para o codelab

O código da solução deste codelab está disponível no GitHub:

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

Se preferir, faça o download do repositório como um arquivo ZIP:

Qual é a próxima etapa?

Confira os outros codelabs no programa de aprendizagem do Compose:

Leia mais