1. Introdução
O Compose é um kit de ferramentas de IU que facilita a implementação de designs em apps. Você descreve como a IU vai ficar e o Compose mostra o resultado na tela. Este codelab ensina a criar IUs do Compose. É necessário já conhecer os conceitos apresentados no codelab de noções básicas. Faça esse codelab primeiro. No codelab de noções básicas, você aprendeu a implementar layouts simples usando Surfaces
, Rows
e Columns
. Você também melhorou esses layouts usando modificadores como padding
, fillMaxWidth
e size
.
Neste codelab, você vai implementar um layout mais realista e complexo e, ao fazer isso, vai aprender sobre diversas funções de composição e modificadores. Ao fim deste codelab, você vai conseguir transformar o design de um app básico em um código que funciona.
Este codelab não acrescenta nenhum comportamento ao app. Para saber mais sobre estado e interação, faça o codelab Como usar o estado no Jetpack Compose.
Para receber mais suporte durante este codelab, confira as orientações neste vídeo (em inglês):
O que você vai aprender
Neste codelab, você vai aprender o seguinte:
- Como os modificadores ajudam a ampliar as funções de composição.
- Como os componentes de layout padrão, como Column e LazyRow, posicionam funções de composição filhas.
- Como os alinhamentos e as disposições mudam a posição dos elementos combináveis filhos no pai deles.
- Como os elementos combináveis do Material Design, por exemplo, o Scaffold e a navegação na parte de baixo da tela, ajudam a criar layouts abrangentes.
- Como criar elementos combináveis flexíveis usando APIs de slot.
- Como criar layouts para diferentes configurações de tela.
O que é necessário
- Versão mais recente do Android.
- Experiência com a sintaxe do Kotlin, incluindo lambdas.
- Experiência básica com o Compose. Concluir o codelab Noções básicas do Jetpack Compose antes deste.
- Conhecimentos básicos sobre elementos combináveis e modificadores.
O que você vai criar
Neste codelab, você vai implementar um design de app realista seguindo modelos fornecidos por um designer. O MySoothe é um app de bem-estar que lista diversas maneiras de melhorar a saúde do seu corpo e da sua mente. Ele contém uma seção que lista suas coleções favoritas e outra seção com exercícios físicos. O app vai ficar assim:
2. Etapas da configuração
Nesta etapa, faça o download do código que contém temas e algumas configurações básicas.
Acessar o código
O código deste codelab pode ser encontrado no repositório codelab-android-compose do GitHub (link em inglês). Para cloná-lo, execute:
$ git clone https://github.com/android/codelab-android-compose
Outra opção é fazer o download de dois arquivos ZIP:
Conferir o código
Você fez o download de um código que contém todos os codelabs disponíveis do Compose. Para concluir este codelab, abra o projeto BasicLayoutsCodelab
no Android Studio.
Recomendamos que você comece com o código na ramificação main
e siga todas as etapas do codelab no seu ritmo.
3. Começar com um plano
Vamos começar implementando o design de retrato do app. Confira mais detalhes:
Ao implementar um design, uma boa maneira de começar é com um entendimento claro da estrutura que vai ser usada. Não comece a programar imediatamente. Em vez disso, analise o design em si. Como é possível dividir a IU em várias partes reutilizáveis?
Vamos tentar fazer isso nesse design. No nível de abstração mais alto, é possível dividir o design em duas partes:
- Conteúdo na tela.
- Navegação na parte de baixo da tela.
Mais detalhadamente, o conteúdo da tela contém três subpartes:
- A barra de pesquisa.
- Uma seção chamada "Align your body" (Alinhe seu corpo).
- Uma seção chamada "Favorite collections" (Coleções favoritas).
Dentro de cada seção, também é possível conferir alguns componentes de nível mais baixo que são reutilizados:
- O elemento "align your body", mostrado em uma linha rolável na horizontal.
- O card "favorite collections", mostrado em uma grade rolável na horizontal.
Agora que analisou o design, você pode começar a implementar funções combináveis para cada parte identificada da interface. Comece com as funções de nível mais baixo e depois vá combinando-as às funções mais complexas. Ao final do codelab, seu novo app vai ficar parecido com o design apresentado.
4. Barra de pesquisa: modificadores
O primeiro elemento a ser transformado em uma função de composição é a barra de pesquisa. Vamos observar o design mais uma vez:
Considerando apenas essa captura de tela, seria muito difícil implementar o design perfeitamente. Geralmente, um designer transmite mais informações sobre o design. Ele pode oferecer acesso à própria ferramenta de design ou compartilhar os chamados "designs vermelhos". No caso do nosso exemplo, o designer enviou os esboços, que podem ser usados para encontrar os valores de dimensionamento. O design é mostrado com uma sobreposição de grade de 8 dp, de modo que é possível notar claramente o espaço deixado entre os elementos e ao redor deles. Além disso, alguns valores de espaçamento são adicionados de modo explícito para esclarecer os tamanhos.
A barra de pesquisa precisa ter uma altura de 56 pixels de densidade independente e preencher toda a largura do contêiner pai.
Para implementar a barra de pesquisa, use um componente do Material Design, conhecido como Campo de texto (em inglês). A biblioteca Compose Material contém uma função de composição conhecida como TextField
, que é a implementação desse componente do Material Design.
Comece com uma implementação básica de TextField
. Na base de código, abra MainActivity.kt
e pesquise a função SearchBar
.
Na função de composição SearchBar
, insira a implementação básica de TextField
:
import androidx.compose.material3.TextField
@Composable
fun SearchBar(
modifier: Modifier = Modifier
) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
É importante observar algumas coisas:
- Você fixou o valor do campo de texto no código, e o callback
onValueChange
não faz nada. Como este codelab tem como foco o layout, vamos ignorar tudo que esteja relacionado ao estado.
- A função de composição
SearchBar
aceita um parâmetromodifier
e o transmite aoTextField
. De acordo com as diretrizes do Compose, essa é a prática recomendada. Assim, o autor da chamada do método pode modificar a aparência da função, fazendo com que ela seja mais flexível e reutilizável. Você vai continuar a aplicar essa prática para todas as funções de composição neste codelab.
Vamos analisar a visualização dessa função. Você pode usar o recurso de visualização do Android Studio para fazer interações rápidas nas funções de composição. MainActivity.kt
contém visualizações de todos os combináveis que você vai criar neste codelab. Nesse caso, o método SearchBarPreview
renderiza a função SearchBar
, adicionando um plano de fundo e um pouco de padding para proporcionar mais contexto. Com a implementação que você acabou de adicionar, a barra vai ficar assim:
Ainda faltam algumas coisas. Primeiro, vamos corrigir o tamanho do elemento combinável usando modificadores.
Ao criar funções de composição, os modificadores são usados para:
- Mudar o tamanho, o layout, o comportamento e a aparência da função.
- Adicionar informações, como rótulos de acessibilidade.
- Processar entradas do usuário.
- Adicionar interações de nível superior, como fazer com que um elemento seja clicável, rolável, arrastável ou redimensionável.
Cada elemento combinável chamado tem um parâmetro modifier
, que pode ser definido para adaptar a aparência e o comportamento desse combinável. Ao definir o modificador, você pode encadear vários métodos de modificadores para criar uma adaptação mais complexa.
Nesse caso, a barra de pesquisa precisa ter pelo menos 56 dp de altura e preencher a largura do contêiner pai. Para encontrar os modificadores certos para isso, consulte a seção "Tamanho" da lista de modificadores. Para a altura, você pode usar o modificador heightIn
. Isso garante que a função de composição tenha uma altura mínima específica. No entanto, esse elemento pode aumentar se o usuário aumenta o tamanho da fonte do sistema, por exemplo. Para a largura, use o modificador fillMaxWidth
. Esse modificador garante que a barra de pesquisa ocupe todo o espaço horizontal do pai.
Atualize o modificador para que ele fique igual ao código abaixo:
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.TextField
@Composable
fun SearchBar(
modifier: Modifier = Modifier
) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
.fillMaxWidth()
.heightIn(min = 56.dp)
)
}
Nesse caso, como um modificador influencia a largura, e o outro a altura, a ordem utilizada não importa.
Também é necessário definir alguns parâmetros do TextField
. Defina os valores dos parâmetros para tentar deixar a função de composição de acordo com o design. Como referência, vamos observar o design novamente:
Siga estas etapas para atualizar a implementação:
- Adicione o ícone de pesquisa. O
TextField
contém um parâmetroleadingIcon
, que aceita outra função de composição. Dentro dele, defina umIcon
, que, nesse caso, é o íconeSearch
. Use a importaçãoIcon
correta do Compose. - Use
TextFieldDefaults.textFieldColors
para substituir cores específicas. Defina afocusedContainerColor
e aunfocusedContainerColor
do campo de texto como a corsurface
do MaterialTheme. - Adicione o texto marcador de posição "Search" (Pesquisar). Você pode encontrá-lo como o recurso de string
R.string.placeholder_search
.
Quando você terminar, a função combinável vai ficar assim:
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.ui.res.stringResource
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
@Composable
fun SearchBar(
modifier: Modifier = Modifier
) {
TextField(
value = "",
onValueChange = {},
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null
)
},
colors = TextFieldDefaults.colors(
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
focusedContainerColor = MaterialTheme.colorScheme.surface
),
placeholder = {
Text(stringResource(R.string.placeholder_search))
},
modifier = modifier
.fillMaxWidth()
.heightIn(min = 56.dp)
)
}
Observe que:
- Você adicionou um
leadingIcon
que mostra o ícone de pesquisa. Esse ícone não precisa incluir uma descrição do conteúdo, porque o marcador já descreve o significado do campo de texto. A descrição de conteúdo geralmente é usada para proporcionar acessibilidade, oferecendo ao usuário uma representação textual de uma imagem ou ícone.
- Para mudar a cor do plano de fundo do campo de texto, defina a propriedade
colors
. O elemento combinável contém um parâmetro combinado, em vez de um parâmetro separado para cada cor. Assim, você vai transmitir uma cópia da classe de dadosTextFieldDefaults
e atualizar apenas as cores que são diferentes. Nesse caso, apenas as coresunfocusedContainerColor
efocusedContainerColor
são diferentes.
Nessa etapa, falamos sobre usar parâmetros combináveis e modificadores para mudar a aparência de um elemento combinável. Essa abordagem é válida para funções de composição fornecidas pelas bibliotecas Compose e Material Design e para aquelas que você programa por conta própria. Assim, é importante sempre incluir parâmetros para personalizar a função de composição que você criar. Também é necessário adicionar uma propriedade modifier
, para que a aparência da função de composição possa ser adaptada de acordo com fatores externos.
5. Align your body: alinhamento
A próxima função de composição que você vai implementar é o elemento "Alinhe seu corpo". Vamos analisar o design e o esboço:
O esboço agora também inclui o espaçamento definido para a linha de base do texto. Nós temos as informações abaixo:
- A imagem precisa ter 88 dp de altura.
- O espaçamento entre a linha de base do texto e a imagem precisa ser de 24 dp.
- O espaçamento entre a linha de base e a parte de baixo do elemento precisa ser de 8 dp.
- O estilo de tipografia do texto precisa ser bodyMedium.
Para implementar essa função de composição, você precisa de uma Image
e um Text
, que vão ser incluídos em uma Column
. Portanto, uma função vai ficar posicionada abaixo da outra.
Encontre o AlignYourBodyElement
no código e atualize o conteúdo dessa função com a seguinte implementação básica:
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.res.painterResource
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
) {
Image(
painter = painterResource(R.drawable.ab1_inversions),
contentDescription = null
)
Text(text = stringResource(R.string.ab1_inversions))
}
}
Observe que:
- Você definiu a
contentDescription
da imagem como nula, porque ela é apenas decorativa. Como o texto abaixo da imagem já descreve bem o significado, não é necessário adicionar uma descrição específica. - A imagem e o texto usados estão codificados. Na próxima etapa, você vai aprender a usar os parâmetros da função
AlignYourBodyElement
para torná-los dinâmicos.
Observe a visualização dessa função de composição:
Ainda é preciso melhorar algumas coisas. A mais perceptível é que a imagem é muito grande e não está em formato de círculo. Você pode adaptar a função de composição Image
usando os modificadores de size
e clip
e o parâmetro contentScale
.
O modificador size
adapta a função de composição para que ela se ajuste a um determinado tamanho, como fillMaxWidth
e heightIn
que apresentamos na etapa anterior. O modificador clip
funciona de forma diferente, adaptando a aparência da função de composição. Ou seja, ele recorta o elemento em qualquer Shape
que você definir.
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
) {
Image(
painter = painterResource(R.drawable.ab1_inversions),
contentDescription = null,
modifier = Modifier
.size(88.dp)
.clip(CircleShape)
)
Text(text = stringResource(R.string.ab1_inversions))
}
}
No momento, o design da visualização está assim:
A imagem também precisa ser dimensionada corretamente. Para fazer isso, podemos usar o parâmetro contentScale
do Image
. Algumas das principais opções são:
Nesse caso, o tipo "Crop" é o certo. Depois de aplicar os modificadores e o parâmetro, seu código vai ficar assim:
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
) {
Image(
painter = painterResource(R.drawable.ab1_inversions),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(88.dp)
.clip(CircleShape)
)
Text( text = stringResource(R.string.ab1_inversions) )
}
}
Agora, o design vai ficar assim:
Agora, defina o alinhamento da Column
para posicionar o texto corretamente na horizontal.
Em geral, para alinhar funções de composição em um contêiner pai, é necessário definir o alinhamento desse contêiner. Assim, em vez de informar à função filha como se posicionar dentro do contêiner pai, você vai informar ao pai como alinhar as filhas.
No caso de uma Column
, é necessário definir como as filhas vão ficar alinhadas horizontalmente. As opções são estas:
- Start
- CenterHorizontally
- End
No caso de uma Row
, você precisa definir o alinhamento vertical. As opções são semelhantes às de Column
:
- Top
- CenterVertically
- Bottom
No caso de uma Box
, é necessário combinar o alinhamento horizontal e vertical. As opções são estas:
- TopStart
- TopCenter
- TopEnd
- CenterStart
- Center
- CenterEnd
- BottomStart
- BottomCenter
- BottomEnd
Todas as filhas do contêiner vão seguir esse mesmo padrão de alinhamento. Você pode adicionar um modificador align
a uma determinada filha para mudar o comportamento dela.
No caso do nosso design, o texto precisa estar centralizado horizontalmente. Para isso, defina o horizontalAlignment
da Column
como centralizada horizontalmente:
import androidx.compose.ui.Alignment
@Composable
fun AlignYourBodyElement(
modifier: Modifier = Modifier
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Image(
//..
)
Text(
//..
)
}
}
Depois de implementar essas partes, restam apenas algumas pequenas mudanças a serem feitas para deixar a função de composição idêntica ao design. Tente implementá-las por conta própria. Caso tenha dificuldades, você pode consultar o código final. Lembre-se de seguir estas etapas:
- Deixe a imagem e o texto dinâmicos. Transmita esses elementos como argumentos para a função de composição. Não se esqueça de atualizar a visualização correspondente e transmitir alguns dados codificados.
- Atualize o texto para usar o estilo de tipografia bodyMedium.
- Atualize os espaçamentos de referência do elemento de texto de acordo com o diagrama.
Ao terminar de implementar essas etapas, o código vai ficar parecido com este:
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
@DrawableRes drawable: Int,
@StringRes text: Int,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(drawable),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(88.dp)
.clip(CircleShape)
)
Text(
text = stringResource(text),
modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
}
}
@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun AlignYourBodyElementPreview() {
MySootheTheme {
AlignYourBodyElement(
text = R.string.ab1_inversions,
drawable = R.drawable.ab1_inversions,
modifier = Modifier.padding(8.dp)
)
}
}
Confira o AlignYourBodyElement na guia "Design".
6. Card "Favorite collections": uso do componente Surface do Material Design
A próxima função de composição a ser implementada é parecido com o elemento "Alinhe seu corpo". Observe o design, que inclui o esboço:
Nesse caso, o tamanho total do elemento combinável foi informado. Observe que o texto precisa ser titleMedium.
Esse contêiner usa surfaceVariant como cor de fundo, que é diferente do plano de fundo de toda a tela. Ela também tem cantos arredondados. Esses detalhes são especificados para o card "favorite collection" usando o elemento combinável Surface
do Material Design.
É possível adaptar a Surface
de acordo com as necessidades do app, definindo parâmetros e modificadores para esse componente. Nesse caso, a superfície precisa ter cantos arredondados. Para isso, use o parâmetro shape
. Em vez de definir a forma como Shape
, como fizemos na imagem da etapa anterior, você vai usar um valor do tema do Material Design.
Vamos conferir qual seria o resultado:
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Surface
@Composable
fun FavoriteCollectionCard(
modifier: Modifier = Modifier
) {
Surface(
shape = MaterialTheme.shapes.medium,
modifier = modifier
) {
Row {
Image(
painter = painterResource(R.drawable.fc2_nature_meditations),
contentDescription = null
)
Text(text = stringResource(R.string.fc2_nature_meditations))
}
}
}
Agora, vamos observar a visualização dessa implementação:
Em seguida, aplique o que você aprendeu na etapa anterior.
- Defina a largura da
Row
e alinhe as filhas verticalmente. - Defina o tamanho da imagem de acordo com o diagrama e corte-a dentro do contêiner.
Tente implementar essas mudanças por conta própria antes de conferir o código da solução.
O código vai ficar parecido com este:
import androidx.compose.foundation.layout.width
@Composable
fun FavoriteCollectionCard(
modifier: Modifier = Modifier
) {
Surface(
shape = MaterialTheme.shapes.medium,
modifier = modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.width(255.dp)
) {
Image(
painter = painterResource(R.drawable.fc2_nature_meditations),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(80.dp)
)
Text(
text = stringResource(R.string.fc2_nature_meditations)
)
}
}
}
E a visualização vai ficar assim:
Para terminar de ajustar essa função combinável, implemente as etapas abaixo:
- Deixe a imagem e o texto dinâmicos. Transmita esses elementos como argumentos para a função combinável.
- Atualize a cor para surfaceVariant.
- Atualize o texto para usar o estilo de tipografia titleMedium.
- Atualize o espaçamento entre a imagem e o texto.
O resultado final vai ficar parecido com este:
@Composable
fun FavoriteCollectionCard(
@DrawableRes drawable: Int,
@StringRes text: Int,
modifier: Modifier = Modifier
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.width(255.dp)
) {
Image(
painter = painterResource(drawable),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(80.dp)
)
Text(
text = stringResource(text),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
}
//..
@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun FavoriteCollectionCardPreview() {
MySootheTheme {
FavoriteCollectionCard(
text = R.string.fc2_nature_meditations,
drawable = R.drawable.fc2_nature_meditations,
modifier = Modifier.padding(8.dp)
)
}
}
Confira a visualização da FavoriteCollectionCardPreview.
7. Linha "Align your body": disposição
Agora que você criou as funções de composição básicas que são exibidas na tela, podemos começar a criar as diferentes seções do app.
Vamos começar com a linha rolável "Align your body".
Observe o esboço desse componente:
Não se esqueça que cada bloco da grade representa 8 dp. Portanto, nesse design, temos um espaço de 16 dp antes do primeiro e depois do último item da linha. Há 8 dp de espaçamento entre cada item.
No Compose, é possível implementar uma linha rolável como essa usando a função LazyRow
. A documentação sobre listas contém muito mais informações sobre listas lentas, como LazyRow
e LazyColumn
. Para este codelab, basta saber que a LazyRow
renderiza apenas os elementos que são exibidos na tela, e não todos os elementos ao mesmo tempo. Isso ajuda a manter um bom desempenho do app.
Para começar, vamos fazer uma implementação básica de LazyRow
:
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
modifier = modifier
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(item.drawable, item.text)
}
}
}
Como podemos notar, as filhas de LazyRow
não são elementos combináveis. Por isso, é necessário usar a DSL de lista lenta, que fornece métodos como item
e items
, responsáveis por emitir funções de composição como itens da lista. Para cada item em alignYourBodyData
, é necessário emitir um AlignYourBodyElement
implementado anteriormente.
Observe como os itens são mostrados:
Ainda precisamos implementar os espaçamentos presentes no esboço. Para isso, é necessário aprender sobre a disposição.
Na etapa anterior, você aprendeu sobre o alinhamento, que é usado para alinhar as filhas de um contêiner em um determinado eixo. No caso de uma Column
, esse eixo é horizontal, enquanto em uma Row
, ele é vertical.
No entanto, também é possível definir como as funções de composição filhas serão posicionadas em relação ao eixo principal de um contêiner, ou seja, horizontal para Row
e vertical para Column
.
No caso de uma Row
, é possível escolher as disposições abaixo:
Já para uma Column
:
Além dessas disposições, também é possível usar o método Arrangement.spacedBy()
para adicionar um espaço fixo entre cada elemento filho.
Nesse caso, é necessário usar o método spacedBy
, porque queremos inserir 8 dp de espaçamento entre cada item na LazyRow
.
import androidx.compose.foundation.layout.Arrangement
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(item.drawable, item.text)
}
}
}
Agora, o design vai ficar assim:
Também precisamos adicionar padding nas laterais da LazyRow
. Nesse caso, adicionar um modificador de padding simples não vai funcionar. Tente adicionar padding à LazyRow
e confira como ela se comporta usando a visualização interativa:
Como você pode notar, ao rolar a linha, o primeiro e o último item visíveis ficam cortados nos dois lados da tela.
Para que seja possível manter o mesmo padding e ainda rolar o conteúdo dentro dos limites da lista mãe sem cortes, todas as listas fornecem um parâmetro chamado contentPadding
à LazyRow
, e o definem como 16.dp
.
import androidx.compose.foundation.layout.PaddingValues
@Composable
fun AlignYourBodyRow(
modifier: Modifier = Modifier
) {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
modifier = modifier
) {
items(alignYourBodyData) { item ->
AlignYourBodyElement(item.drawable, item.text)
}
}
}
Use a visualização interativa para conferir a diferença que o padding faz.
8. Grade "Favorite collections": grades lentas
A próxima seção a ser implementada é a parte "Coleções favoritas" da tela. Em vez de uma única linha, essa função precisa de uma grade:
É possível implementar essa seção de forma semelhante à anterior, criando uma LazyRow
e permitindo que cada item contenha uma Column
com duas instâncias de FavoriteCollectionCard
. Mas, nesta etapa, vamos usar a LazyHorizontalGrid
, que proporciona um melhor posicionamento dos itens como elementos de grade.
Comece com uma implementação simples da grade com duas linhas fixas:
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
@Composable
fun FavoriteCollectionsGrid(
modifier: Modifier = Modifier
) {
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
modifier = modifier
) {
items(favoriteCollectionsData) { item ->
FavoriteCollectionCard(item.drawable, item.text)
}
}
}
Como você pode notar, basta substituir a LazyRow
da etapa anterior por uma LazyHorizontalGrid
. Essa implementação ainda não vai gerar o resultado certo:
A grade ocupa o mesmo espaço que o contêiner pai, fazendo com que os cards das coleções favoritas fiquem muito esticados na vertical.
Adapte o elemento combinável para que:
- A grade tenha um contentPadding horizontal de 16.dp.
- A organização horizontal e vertical tenha um espaçamento de 16.dp.
- A altura da grade seja de 168.dp.
- O modificador do FavoriteCollectionCard especifique uma altura de 80.dp.
O código final ficará assim:
@Composable
fun FavoriteCollectionsGrid(
modifier: Modifier = Modifier
) {
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier.height(168.dp)
) {
items(favoriteCollectionsData) { item ->
FavoriteCollectionCard(item.drawable, item.text, Modifier.height(80.dp))
}
}
}
A visualização ficará assim:
9. Tela inicial: APIs de slot
Na tela inicial do app MySoothe, há várias seções que seguem um mesmo padrão. Cada uma tem um título, e os conteúdos variam de acordo com a seção. O esboço do design que queremos implementar é este:
Como podemos notar, cada seção tem um título e um slot. Há algumas informações de espaçamento e estilo associadas ao título. Já o slot pode ser preenchido de maneira dinâmica com conteúdos diferentes, de acordo com cada seção.
Para implementar esse contêiner de seção flexível, é necessário usar as APIs de slot. Antes de fazer isso, leia a seção sobre layouts baseados em slot na página de documentação. Ela vai ajudar você a entender o que é um layout baseado em slots e como usar as APIs de slots para criar esse layout.
Adapte o elemento combinável HomeSection
para usar o título e o conteúdo do slot. Também é necessário adaptar a visualização associada para chamar a HomeSection
com o título e o conteúdo do elemento "Align your body":
@Composable
fun HomeSection(
@StringRes title: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Column(modifier) {
Text(stringResource(title))
content()
}
}
@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun HomeSectionPreview() {
MySootheTheme {
HomeSection(R.string.align_your_body) {
AlignYourBodyRow()
}
}
}
Você pode usar o parâmetro content
para o slot do elemento combinável. Ao usar o elemento HomeSection
, você pode implementar uma lambda final para preencher o slot do conteúdo. Para casos em que um elemento de composição fornece vários slots a serem preenchidos, é possível designar nomes diferentes que representem a função de cada slot dentro do contêiner. Por exemplo, o TopAppBar
(em inglês) do Material Design fornece slots para title
, navigationIcon
e actions
.
Vamos conferir como a seção vai ficar com essa implementação:
Ainda precisamos adicionar mais informações ao elemento de texto para que ele fique de acordo com o design.
Portanto, vamos atualizá-lo para que:
- Ele use a tipografia titleMedium.
- O espaçamento entre o valor de referência do texto e a parte de cima seja de 40 dp.
- O espaçamento entre o valor de referência do texto e a parte de baixo do elemento seja de 16 dp.
- O padding horizontal seja de 16 dp.
A solução final vai ficar assim:
@Composable
fun HomeSection(
@StringRes title: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Column(modifier) {
Text(
text = stringResource(title),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.paddingFromBaseline(top = 40.dp, bottom = 16.dp)
.padding(horizontal = 16.dp)
)
content()
}
}
10. Tela inicial: rolagem
Agora que você criou todos os elementos básicos separadamente, é possível combiná-los em uma implementação de tela cheia.
Vamos analisar o design que queremos implementar:
Estamos simplesmente colocando a barra de pesquisa e as duas seções abaixo uma da outra. É necessário adicionar espaçamento para que tudo se encaixe no design. Até agora, não usamos o Spacer
, um elemento combinável que ajuda a inserir um espaço extra na Column
. Caso você definisse o padding da Column
, em vez de usar esse elemento, ocorreria o mesmo comportamento que observamos na grade "Favorite collections", em que as imagens ficaram cortadas nos dois lados da tela.
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
Column(modifier) {
Spacer(Modifier.height(16.dp))
SearchBar(Modifier.padding(horizontal = 16.dp))
HomeSection(title = R.string.align_your_body) {
AlignYourBodyRow()
}
HomeSection(title = R.string.favorite_collections) {
FavoriteCollectionsGrid()
}
Spacer(Modifier.height(16.dp))
}
}
Embora esse design se ajuste bem à maioria dos tamanhos de dispositivo, ele ainda precisa ser rolável verticalmente para casos em que a tela não é alta o suficiente, por exemplo, no modo paisagem. Para isso, precisamos adicionar o comportamento de rolagem.
Como explicado anteriormente, os layouts lentos, por exemplo, LazyRow
e LazyHorizontalGrid
, adicionam automaticamente o comportamento de rolagem. No entanto, nem sempre você precisa de um layout lento. Em geral, o layout lento é usado quando há muitos elementos em uma lista ou grandes conjuntos de dados a serem carregados. Isso porque, nesses casos, emitir todos os itens de uma só vez poderia prejudicar a performance e causar lentidão no app. Quando uma lista tem um número limitado de elementos, é possível usar uma Column
ou uma Row
simples e adicionar o comportamento de rolagem manualmente. Para isso, use os modificadores verticalScroll
ou horizontalScroll
. Eles exigem que o ScrollState
seja informado, contendo o estado atual da rolagem, que é usado para modificar o estado de rolagem de acordo com fatores externos. Nesse caso, não precisamos modificar o estado de rolagem, então, basta criar uma instância ScrollState
persistente usando rememberScrollState
.
O resultado final vai ficar assim:
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
Column(
modifier
.verticalScroll(rememberScrollState())
) {
Spacer(Modifier.height(16.dp))
SearchBar(Modifier.padding(horizontal = 16.dp))
HomeSection(title = R.string.align_your_body) {
AlignYourBodyRow()
}
HomeSection(title = R.string.favorite_collections) {
FavoriteCollectionsGrid()
}
Spacer(Modifier.height(16.dp))
}
}
Para verificar o comportamento de rolagem do elemento combinável, limite a altura da visualização e a execute no modo interativo:
@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE, heightDp = 180)
@Composable
fun ScreenContentPreview() {
MySootheTheme { HomeScreen() }
}
11. Navegação na parte de baixo da tela: Material Design
Agora que o conteúdo da tela foi implementado, está tudo pronto para adicionar a decoração. No caso do MySoothe, há uma barra de navegação que permite alternar entre telas diferentes.
Primeiro, implemente o elemento combinável da barra de navegação e inclua-o no app.
Vamos observar o design:
Felizmente, não é necessário implementar essa função combinável inteira do zero. Você pode usar a NavigationBar
, que faz parte da biblioteca Compose Material. No elemento combinável NavigationBar
, é possível adicionar um ou mais elementos NavigationBarItem
, que vão seguir o estilo definido pela biblioteca do Material Design automaticamente.
Comece com uma implementação básica dessa navegação:
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
NavigationBar(
modifier = modifier
) {
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(
text = stringResource(R.string.bottom_navigation_home)
)
},
selected = true,
onClick = {}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(
text = stringResource(R.string.bottom_navigation_profile)
)
},
selected = false,
onClick = {}
)
}
}
Esta é a aparência da implementação básica. Não há muito contraste entre a cor do conteúdo e a cor da barra de navegação:
Precisamos fazer algumas adaptações de estilo. Primeiro, defina o parâmetro containerColor
para atualizar a cor do plano de fundo da navegação. Use a cor surfaceVariant do tema do Material Design. A solução final vai ficar assim:
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
modifier = modifier
) {
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
NavigationBarItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
A barra de navegação ficará assim. Confira como agora ela tem mais contraste:
12. App MySoothe: scaffold
Nesta etapa, vamos criar a implementação em tela cheia e incluir o elemento de navegação na parte de baixo da tela. Para isso, use o elemento combinável Scaffold
do Material Design. O Scaffold
oferece uma função configurável de nível superior para apps que implementam o Material Design. Ele contém slots para diversos conceitos do Material Design, incluindo a barra na parte de baixo da tela. Você pode posicionar a função combinável de navegação criada na etapa anterior nessa barra.
Implemente o elemento combinável MySootheAppPortrait()
. Esse é o elemento de nível superior do app, portanto, faça o seguinte:
- Aplique o tema
MySootheTheme
do Material Design. - Adicione o
Scaffold
. - Defina a barra da parte de baixo da tela como a função
SootheBottomNavigation
. - Defina o conteúdo como
HomeScreen
.
O resultado final vai ficar assim:
import androidx.compose.material3.Scaffold
@Composable
fun MySootheAppPortrait() {
MySootheTheme {
Scaffold(
bottomBar = { SootheBottomNavigation() }
) { padding ->
HomeScreen(Modifier.padding(padding))
}
}
}
Você concluiu a implementação. Caso queira conferir se a versão que você criou foi implementada perfeitamente, compare esta imagem à sua própria visualização:
13. Coluna de navegação: Material Design
Ao criar layouts para apps, você também precisa saber como eles ficarão em várias configurações, incluindo o modo paisagem no smartphone. Confira o design do app no modo paisagem. Observe como o elemento de navegação da parte de baixo se transforma em uma coluna à esquerda do conteúdo da tela:
Para implementar esse design, use o elemento combinável NavigationRail
, que faz parte da biblioteca Compose Material e tem uma implementação semelhante à NavigationBar
usada para criar a barra de navegação da parte de baixo. No elemento combinável NavigationRail, você vai adicionar elementos NavigationRailItem
para a página inicial e o perfil:
Vamos começar com a implementação básica para uma coluna de navegação:
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
NavigationRail(
) {
Column(
) {
NavigationRailItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
NavigationRailItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
}
Precisamos fazer algumas adaptações de estilo.
- Adicione 8.dp de padding no início e no fim da coluna.
- Para atualizar a cor do plano de fundo da coluna de navegação, defina o parâmetro
containerColor
usando a cor do plano de fundo do tema do Material Design. Ao definir a cor do plano de fundo, a cor dos ícones e dos textos vão se adaptar automaticamente à coronBackground
do tema. - A coluna precisa preencher a altura máxima.
- Defina a organização vertical da coluna como centralizada.
- Defina o alinhamento horizontal da coluna como centralizado horizontalmente.
- Adicione 8.dp de padding entre os dois ícones.
A solução final vai ficar assim:
import androidx.compose.foundation.layout.fillMaxHeight
@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
NavigationRail(
modifier = modifier.padding(start = 8.dp, end = 8.dp),
containerColor = MaterialTheme.colorScheme.background,
) {
Column(
modifier = modifier.fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
NavigationRailItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
Spacer(modifier = Modifier.height(8.dp))
NavigationRailItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
}
Agora, vamos adicionar a coluna de navegação ao layout de paisagem:
Na versão retrato do app, você usou um Scaffold. No entanto, para a paisagem, você usará uma linha e colocará a coluna de navegação e o conteúdo da tela lado a lado.
@Composable
fun MySootheAppLandscape() {
MySootheTheme {
Row {
SootheNavigationRail()
HomeScreen()
}
}
}
Quando você usou um Scaffold na versão retrato, ele também definiu a cor do conteúdo como a cor do plano de fundo. Para configurar a cor da coluna de navegação, una a linha em uma superfície e defina-a como a cor do plano de fundo:
@Composable
fun MySootheAppLandscape() {
MySootheTheme {
Surface(color = MaterialTheme.colorScheme.background) {
Row {
SootheNavigationRail()
HomeScreen()
}
}
}
}
14. App MySoothe: tamanho da janela
A visualização do modo paisagem está ótima. No entanto, se você executar o app em um dispositivo ou emulador e o virar para o lado, ele não mostrará essa orientação. Isso acontece porque precisamos informar ao app quando mostrar cada configuração. Para fazer isso, use a função calculateWindowSizeClass()
para conferir em qual modo o smartphone está:
Existem três larguras de classe de tamanho de janela: compacto, médio e expandido. No modo retrato, a largura é compacta, enquanto no modo paisagem ela é expandida. Neste codelab você não trabalhará com a largura média.
Atualize o elemento combinável MySootheApp para usar a WindowSizeClass do dispositivo. Se a largura for compacta, transmita a versão retrato do app. Se for expandida, transmita a versão paisagem.
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
when (windowSize.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
MySootheAppPortrait()
}
WindowWidthSizeClass.Expanded -> {
MySootheAppLandscape()
}
}
}
Em setContent()
, crie um valor chamado windowSizeClass definido como calculateWindowSize()
e transmita-o para MySootheApp().
Como o calculateWindowSize()
ainda é experimental, será necessário ativar a classe ExperimentalMaterial3WindowSizeClassApi
.
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val windowSizeClass = calculateWindowSizeClass(this)
MySootheApp(windowSizeClass)
}
}
}
Agora, execute o app no emulador ou dispositivo e confira como a tela muda ao girar.
15. Parabéns
Parabéns! Você concluiu este codelab e aprendeu mais sobre layouts no Compose. Ao implementar um design real a um app, você aprendeu sobre modificadores, alinhamentos, disposições, layouts lentos, APIs de slot, rolagem, componentes do Material Design e designs específicos para layouts.
Confira os outros codelabs no Programa de treinamentos do Compose. Consulte também os exemplos de código (link em inglês).
Documentação
Para mais informações e orientações sobre esses temas, consulte as documentações abaixo: