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 combináveis, que são funções normais marcadas com @Composable
e que podem chamar outras funções combináveis. 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 combináveis costumam ser chamadas de "combináveis" para abreviar.
Ao criar pequenas funções combináveis que podem ser reutilizadas, é fácil criar uma biblioteca de elementos de interface 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):
Observação: o Material 2 é usado no vídeo, mas este codelab foi atualizado para uso do Material 3. Portanto, algumas etapas serão diferentes.
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 combináveis
- 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:
O que é necessário
2. Como iniciar um novo projeto do Compose
Para iniciar um novo projeto do Compose, abra o Android Studio.
Se estiver na janela Welcome to Android Studio, clique em Start a new Android Studio project. Caso você já tenha um projeto aberto no Android Studio, selecione File > New > New Project na barra de menus.
Ao criar um novo projeto, selecione a opção Empty Activity nos modelos disponíveis.
Clique em Next e configure o projeto normalmente com o nome "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 Activity, o código abaixo será gerado no seu projeto:
- O projeto já está configurado para usar o Compose.
- O arquivo
AndroidManifest.xml
foi criado. - Os arquivos
build.gradle.kts
eapp/build.gradle.kts
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(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
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/android/codelab-android-compose
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 as etapas mostradas no codelab em seu próprio ritmo e, se necessário, consulte a solução. Durante o codelab, você vai encontrar snippets de código que precisam ser adicionados ao projeto.
3. Começar a usar o Compose
Analise as classes e os métodos diferentes relacionados ao Compose que foram gerados pelo Android Studio.
Funções combináveis
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 combinável fornecida pela biblioteca.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
Compose em um app Android
No Compose, a Activity
ainda é 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 combináveis.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
BasicsCodelabTheme
é uma maneira de definir o estilo de funções combináveis. 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 GreetingPreview() {
BasicsCodelabTheme {
Greeting(name = "Android")
}
}
A visualização talvez não apareça se a opção Code estiver selecionada. Clique em Split 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.colorScheme.primary
.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
}
Os componentes aninhados em Surface
serão renderizados por cima da cor do segundo plano.
Confira as novas mudanças na visualização:
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.material3.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 interface como serão dispostos, mostrados ou se comportarão no layout pai. Você já deve ter notado que o elemento combinável Greeting
já tem um modificador padrão, que é transmitido para o Text
.
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()
. Também é possível adicionar vários modificadores com o encadeamento. Nesse caso, podemos adicionar o modificador de padding ao padrão: modifier.padding(24.dp)
.
Agora, adicione padding ao Text
na tela:
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
// ...
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text(
text = "Hello $name!",
modifier = modifier.padding(24.dp)
)
}
}
Existem dezenas de modificadores que podem ser usados para alinhar, animar, dispor, tornar clicáveis ou roláveis, transformar etc. Para uma lista abrangente, 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ê vai criar. Isso poderá 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.
Como prática recomendada, a função precisa incluir um parâmetro de modificador atribuído a um modificador vazio por padrão. Encaminhe esse modificador para o primeiro elemento combinável chamado dentro da função. Assim, o local da chamada pode adaptar instruções e comportamentos de layout fora da função de composição.
Crie um elemento combinável chamado MyApp
que inclua a saudação.
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
Isso permite limpar o callback onCreate
e a visualização, porque é possível reutilizar o combinável MyApp
, evitando a duplicação de código.
Na visualização, vamos chamar MyApp
e remover o nome da visualização.
Seu arquivo MainActivity.kt
vai ficar assim:
package com.example.basicscodelab
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.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.example.basicscodelab.ui.theme.BasicsCodelabTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primary) {
Text(
text = "Hello $name!",
modifier = modifier.padding(24.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
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
.
Essas são funções de composição, ou seja, você pode colocar itens nelas. Por exemplo, cada filho dentro de uma Column
vai 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:
Talvez seja necessário mover o padding.
Compare seu resultado com esta solução:
import androidx.compose.foundation.layout.Column
// ...
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.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 interfaces muito eficiente, já que é possível adicionar instruções para influenciar como a interface vai ser mostrada.
Por exemplo, é possível usar uma repetição for
para adicionar elementos à Column
:
@Composable
fun MyApp(
modifier: Modifier = Modifier,
names: List<String> = listOf("World", "Compose")
) {
Column(modifier) {
for (name in names) {
Greeting(name = name)
}
}
}
Você ainda não definiu dimensões ou não adicionou restrições ao tamanho dos combináveis. 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 GreetingPreview() {
BasicsCodelabTheme {
MyApp()
}
}
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
.
Agora, compare o código com a solução:
import androidx.compose.foundation.layout.fillMaxWidth
@Composable
fun MyApp(
modifier: Modifier = Modifier,
names: List<String> = listOf("World", "Compose")
) {
Column(modifier = modifier.padding(vertical = 4.dp)) {
for (name in names) {
Greeting(name = name)
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(
color = MaterialTheme.colorScheme.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
. Precisamos adicionar esse botão primeiro. O objetivo é criar o layout abaixo:
Button
é um combinável fornecido pelo pacote do Material 3, que usa um combinável 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 peso, 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.foundation.layout.Row
import androidx.compose.material3.ElevatedButton
// ...
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
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:
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
. Confira esse booleano expanded
e como ele é usado no código:
// Don't copy over
@Composable
fun Greeting(name: String) {
var expanded = false // Don't do this!
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
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. Vamos explicar melhor esses componentes mais adiante.
No entanto, isso não funcionará como esperado. Definir um valor diferente para a variável expanded
não vai fazer 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 vai ser redefinida como falsa.
Para adicionar um estado interno a um combinável, 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, vai 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 aberto e mostrar um texto diferente, dependendo do valor.
ElevatedButton(
onClick = { expanded.value = !expanded.value },
) {
Text(if (expanded.value) "Show less" else "Show more")
}
Execute o app no modo interativo para conferir o comportamento.
Quando o botão é clicado, o expanded
é alternado, acionando uma recomposição do texto dentro dele. Cada Greeting
mantém o próprio estado aberto porque pertence a diferentes elementos da interface.
Codifique até este ponto:
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val expanded = remember { mutableStateOf(false) }
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
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
fun Greeting(name: String, modifier: Modifier = Modifier) {
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 está fazendo um cálculo simples.
Agora podemos aplicar um novo modificador de padding à coluna:
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val expanded = remember { mutableStateOf(false) }
val extraPadding = if (expanded.value) 48.dp else 0.dp
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}
Se você executar em um emulador ou no modo interativo, vai notar que cada item pode ser aberto de maneira independente:
8. Elevação de estado
Nas funções combináveis, 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 funções combináveis e facilita muito o teste delas. Por outro lado, o estado que não precisa ser controlado pelo pai de uma função combinável 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.
Adicione o código abaixo ao arquivo MainActivity.kt
:
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
// ...
@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
// TODO: This state should be hoisted
var shouldShowOnboarding by remember { mutableStateOf(true) }
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 um novo elemento combinável chamado
OnboardingScreen
e uma nova visualização. Se você criar o projeto, vai 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-chaveby
em vez de=
. É um delegado de propriedade que evita que você digite.value
todas as vezes.- Quando o botão é clicado,
shouldShowOnboarding
é definido comofalse
, 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 que ela apareça na inicialização e depois seja ocultada quando o usuário pressionar "Continue".
No Compose, você não oculta elementos de interface. Em vez disso, você simplesmente não os adiciona à composição. 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(modifier: Modifier = Modifier) {
Surface(modifier) {
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 o mover para o ancestral comum que precisa acessá-lo.
Primeiro, mova o conteúdo de MyApp
para uma nova composição, chamada Greetings
: Adapte também a visualização para chamar o método Greetings
:
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Greetings()
}
@Composable
private fun Greetings(
modifier: Modifier = Modifier,
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)
@Composable
fun GreetingsPreview() {
BasicsCodelabTheme {
Greetings()
}
}
Adicione uma visualização do novo elemento combinável MyApp de nível superior para que possamos testar o comportamento dele:
@Preview
@Composable
fun MyAppPreview() {
BasicsCodelabTheme {
MyApp(Modifier.fillMaxSize())
}
}
Agora adicione a lógica para mostrar as diferentes telas no MyApp
e elevar o estado.
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by remember { mutableStateOf(true) }
Surface(modifier) {
if (shouldShowOnboarding) {
OnboardingScreen(/* TODO */)
} else {
Greetings()
}
}
}
Também precisamos compartilhar shouldShowOnboarding
com a tela de integração, mas a transmissão não vai ser direta. 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(modifier: Modifier = Modifier) {
var shouldShowOnboarding by remember { mutableStateOf(true) }
Surface(modifier) {
if (shouldShowOnboarding) {
OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
} else {
Greetings()
}
}
}
@Composable
fun OnboardingScreen(
onContinueClicked: () -> Unit,
modifier: Modifier = Modifier
) {
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!
No elemento combinável MyApp
, usamos o delegado da propriedade by
pela primeira vez para evitar sempre o uso do valor. Vamos usar by
em vez de =
também no elemento combinável de "Greeting" da propriedade expanded
. Mude expanded
de val
para var
.
Código completo até agora:
package com.example.basicscodelab
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.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.theme.BasicsCodelabTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by remember { mutableStateOf(true) }
Surface(modifier) {
if (shouldShowOnboarding) {
OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
} else {
Greetings()
}
}
}
@Composable
fun OnboardingScreen(
onContinueClicked: () -> Unit,
modifier: Modifier = Modifier
) {
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(
modifier: Modifier = Modifier,
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
fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by remember { mutableStateOf(false) }
val extraPadding = if (expanded) 48.dp else 0.dp
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greetings()
}
}
@Preview
@Composable
fun MyAppPreview() {
BasicsCodelabTheme {
MyApp(Modifier.fillMaxSize())
}
}
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 e preenchê-la com o valor contido na 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(
modifier: Modifier = Modifier,
names: List<String> = List(1000) { "$it" }
) {
LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
items(items = names) { name ->
Greeting(name = name)
}
}
}
10. Como manter o estado
Nosso app tem dois problemas:
Manter o estado da tela de integração
Se você executar o app em um dispositivo, clicar nos botões e girar, a tela de integração vai aparecer de novo. A função remember
funciona somente enquanto o elemento combinável for mantido. Quando você faz a rotação, toda a atividade é reiniciada e o estado é 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
:
import androidx.compose.runtime.saveable.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.
Manter o estado aberto dos itens da lista
Se você abrir um item da lista e depois rolar a lista até que ele fique fora da visualização ou girar o dispositivo e depois voltar ao item aberto, vai notar que o item voltou ao estado inicial.
A solução para isso também é usar rememberSaveable para o estado aberto:
var expanded by rememberSaveable { mutableStateOf(false) }
Com cerca de 120 linhas de código até agora, você conseguiu mostrar uma lista de rolagem longa de itens e com alta performance, em que cada item mantém o próprio estado. Além disso, como você pode notar, 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ê vai 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:
Para isso, você vai usar o combinável 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 aberto.
import androidx.compose.animation.core.animateDpAsState
@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
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:
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { 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 pode causar falhas no app. Isso introduz um bug de animação sutil que corrigiremos mais tarde na seção 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:
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
vai reiniciar a animação e apontar para o novo valor. As interrupções parecem especialmente naturais com animações com molas:
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
package com.example.basicscodelab
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.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.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.theme.BasicsCodelabTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
Surface(modifier) {
if (shouldShowOnboarding) {
OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
} else {
Greetings()
}
}
}
@Composable
fun OnboardingScreen(
onContinueClicked: () -> Unit,
modifier: Modifier = Modifier
) {
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(
modifier: Modifier = Modifier,
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, modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
Surface(
color = MaterialTheme.colorScheme.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)
}
ElevatedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greetings()
}
}
@Preview
@Composable
fun MyAppPreview() {
BasicsCodelabTheme {
MyApp(Modifier.fillMaxSize())
}
}
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/Theme.kt
, verá que BasicsCodelabTheme
usa MaterialTheme
na implementação:
// Do not copy
@Composable
fun BasicsCodelabTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
// ...
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
MaterialTheme
é uma função combinável que reflete os princípios de estilo da especificação do Material Design (em inglês). 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(modifier = Modifier.fillMaxSize())
}
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
: colorScheme
, typography
e shapes
. Use-as para definir o estilo de cabeçalho como um dos Text
s:
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))
) {
Text(text = "Hello, ")
Text(text = name, style = MaterialTheme.typography.headlineMedium)
}
A composição Text
no exemplo acima define um novo TextStyle
. Você pode criar seu próprio TextStyle
ou extrair um estilo definido pelo tema usando MaterialTheme.typography
, que é preferencial. Essa construção oferece acesso aos estilos de texto definidos pelo Material Design, como displayLarge, headlineMedium, titleSmall, bodyLarge, labelMedium
. No exemplo, usamos o estilo headlineMedium
, definido no tema.
Crie o app agora para conferir nosso texto com estilo recém-definido:
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:
import androidx.compose.ui.text.font.FontWeight
// ...
Text(
text = name,
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.ExtraBold
)
)
Dessa forma, se você precisar mudar a família de fontes ou qualquer outro atributo de headlineMedium
, não vai ter que se preocupar com os pequenos desvios.
Este vai ser o resultado na janela de visualização:
Configurar uma visualização do modo escuro
Por enquanto, a prévia mostra apenas como o app vai ficar no modo claro. Adicione uma anotação @Preview
extra a GreetingPreview
com UI_MODE_NIGHT_YES
:
import android.content.res.Configuration.UI_MODE_NIGHT_YES
@Preview(
showBackground = true,
widthDp = 320,
uiMode = UI_MODE_NIGHT_YES,
name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greetings()
}
}
Isso adiciona uma visualização no modo escuro.
Ajustar o tema do app
Tudo o que está relacionado ao tema atual pode ser encontrado nos arquivos dentro da pasta ui/theme
. 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 LightColorScheme = lightColorScheme(
surface = Blue,
onSurface = Color.White,
primary = LightBlue,
onPrimary = Navy
)
Se você voltar para MainActivity.kt
e atualizar a visualização, as cores não vão mudar. Isso ocorre porque, por padrão, a visualização usa cores dinâmicas. Você pode ver a lógica para adicionar cores dinâmicas em Theme.kt
, usando o parâmetro booleano dynamicColor
.
Para ver a versão não adaptável do esquema de cores, execute o app em um dispositivo com API de nível anterior a 31 (que corresponde ao Android S, em que as cores adaptáveis foram introduzidas). Agora você vai ver as novas cores:
Em Theme.kt
, defina a paleta para cores escuras:
private val DarkColorScheme = darkColorScheme(
surface = Blue,
onSurface = Navy,
primary = Navy,
onPrimary = Chartreuse
)
Agora, ao executar o app, podemos ver como as cores ficam no modo escuro:
Código final para Theme.kt
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
private val DarkColorScheme = darkColorScheme(
surface = Blue,
onSurface = Navy,
primary = Navy,
onPrimary = Chartreuse
)
private val LightColorScheme = lightColorScheme(
surface = Blue,
onSurface = Color.White,
primary = LightBlue,
onPrimary = Navy
)
@Composable
fun BasicsCodelabTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
(view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
13. Toques finais!
Nesta etapa, você vai aplicar o que já sabe e aprender novos conceitos com apenas algumas dicas. Você vai criar o seguinte:
Substituir o botão por um ícone
- Use a composição
IconButton
com um filhoIcon
. - Use
Icons.Filled.ExpandLess
eIcons.Filled.ExpandMore
, que estão disponíveis no artefatomaterial-icons-extended
. Adicione a seguinte linha às dependências no arquivoapp/build.gradle.kts
:
implementation("androidx.compose.material:material-icons-extended")
- 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 deGreeting
que vai ser exibida quando o item for expandido. - Remova o
extraPadding
e aplique o modificadoranimateContentSize
à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 usarcoerceAtLeast
.
Adicionar elevação e formas
- Você pode usar o modificador
shadow
com o modificadorclip
para ter a aparência do card. No entanto, há uma composição do Material Design que faz exatamente isso:Card
. Para mudar as cores doCard
, chameCardDefaults.cardColors
e substitua a cor que você quer mudar.
Código final
package com.example.basicscodelab
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.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.example.basicscodelab.ui.theme.BasicsCodelabTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
Surface(modifier, color = MaterialTheme.colorScheme.background) {
if (shouldShowOnboarding) {
OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
} else {
Greetings()
}
}
}
@Composable
fun OnboardingScreen(
onContinueClicked: () -> Unit,
modifier: Modifier = Modifier
) {
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(
modifier: Modifier = Modifier,
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, modifier: Modifier = Modifier) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
),
modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
CardContent(name)
}
}
@Composable
private fun CardContent(name: String) {
var expanded by rememberSaveable { 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.headlineMedium.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 = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
Greetings()
}
}
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
BasicsCodelabTheme {
OnboardingScreen(onContinueClicked = {})
}
}
@Preview
@Composable
fun MyAppPreview() {
BasicsCodelabTheme {
MyApp(Modifier.fillMaxSize())
}
}
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/android/codelab-android-compose
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
- Conceitos básicos do Jetpack Compose junto com o código
- Guia Modelo mental do Compose
- Apps de exemplo para ver o Compose em ação.