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.
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 android-compose-codelabs do GitHub (link em inglês). Para clonar, execute o seguinte:
$ git clone https://github.com/googlecodelabs/android-compose-codelabs
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 observar o design mais detalhadamente:
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 de composição para cada parte identificada da IU. 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.material.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 todas as funções de composição 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.material.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. - Defina a cor do plano de fundo do campo de texto como a cor
surface
do MaterialTheme. Você pode usarTextFieldDefaults.textFieldColors
para substituir cores específicas. - 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 de composição vai ficar assim:
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.ui.res.stringResource
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.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.textFieldColors(
backgroundColor = MaterialTheme.colors.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 de composição 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 a cor do plano de fundo é diferente. - Você definiu uma altura mínima, e não fixa. Essa é a abordagem recomendada para que ainda seja possível aumentar o campo de texto quando o usuário aumentar o tamanho da fonte nas configurações do sistema, por exemplo.
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 H3.
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
AlightYourBodyElement
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.
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 correto.
- Atualize o espaçamento da linha de base do elemento de texto.
Ao terminar de implementar essas etapas, o código vai ficar parecido com este:
import androidx.compose.foundation.layout.paddingFromBaseline
@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),
style = MaterialTheme.typography.h3,
modifier = Modifier.paddingFromBaseline(
top = 24.dp, bottom = 8.dp
)
)
}
}
@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun AlignYourBodyElementPreview() {
MySootheTheme {
AlignYourBodyElement(
text = R.string.ab1_inversions,
drawable = R.drawable.ab1_inversions,
modifier = Modifier.padding(8.dp)
)
}
}
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 da função de composição foi informado e, mais uma vez, o estilo do texto precisa ser H3.
A cor do plano de fundo desse contêiner é diferente da usada na tela inteira. Ela também tem cantos arredondados. Como o designer não especificou uma cor, podemos presumir que a cor vai ser definida pelo tema. No caso desse contêiner, vamos usar a função de composição 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.material.Surface
@Composable
fun FavoriteCollectionCard(
modifier: Modifier = Modifier
) {
Surface(
shape = MaterialTheme.shapes.small,
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 o tamanho da imagem e corte-a de acordo com o formato do contêiner. Defina a largura da Row
e alinhe as filhas verticalmente. Tente implementar essas mudanças por conta própria antes de ver 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.small,
modifier = modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.width(192.dp)
) {
Image(
painter = painterResource(R.drawable.fc2_nature_meditations),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(56.dp)
)
Text(
text = stringResource(R.string.fc2_nature_meditations)
)
}
}
}
E a visualização vai ficar assim:
Para terminar de ajustar essa função de composição, implemente as etapas abaixo:
- Deixe a imagem e o texto dinâmicos. Transmita esses elementos como argumentos para a função de composição.
- Atualize o texto para usar o estilo de tipografia correto.
- 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.small,
modifier = modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.width(192.dp)
) {
Image(
painter = painterResource(drawable),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(56.dp)
)
Text(
text = stringResource(text),
style = MaterialTheme.typography.h3,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
}
//..
@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2)
@Composable
fun FavoriteCollectionCardPreview() {
MySootheTheme {
FavoriteCollectionCard(
text = R.string.fc2_nature_meditations,
drawable = R.drawable.fc2_nature_meditations,
modifier = Modifier.padding(8.dp)
)
}
}
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 o que acontece:
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 conhecido como contentPadding
.
@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)
}
}
}
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. Para que as células da grade tenham o tamanho e o espaçamento correto entre elas, é necessário adaptar a função de composição.
O resultado vai ficar assim:
@Composable
fun FavoriteCollectionsGrid(
modifier: Modifier = Modifier
) {
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier.height(120.dp)
) {
items(favoriteCollectionsData) { item ->
FavoriteCollectionCard(
drawable = item.drawable,
text = item.text,
modifier = Modifier.height(56.dp)
)
}
}
}
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 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 de composição 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 = 0xFFF0EAE2)
@Composable
fun HomeSectionPreview() {
MySootheTheme {
HomeSection(R.string.align_your_body) {
AlignYourBodyRow()
}
}
}
Você pode usar o parâmetro content
para o slot do elemento de composição. 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:
- O texto apareça em letras maiúsculas. Dica: você pode usar o método
uppercase()
daString
para isso. - O texto use a tipografia H2.
- Os paddings do texto correspondam ao esboço do design.
A solução final vai ficar assim:
import java.util.*
@Composable
fun HomeSection(
@StringRes title: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Column(modifier) {
Text(
text = stringResource(title).uppercase(Locale.getDefault()),
style = MaterialTheme.typography.h2,
modifier = Modifier
.paddingFromBaseline(top = 40.dp, bottom = 8.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ós ainda não usamos o Spacer
, um elemento de composição que ajuda a inserir espaço extra em uma 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.rememberScrollState
import androidx.compose.foundation.verticalScroll
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
Column(
modifier
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
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 de composição, limite a altura da visualização e a execute no modo interativo:
@Preview(showBackground = true, backgroundColor = 0xFFF0EAE2, 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 app MySoothe, há uma função de navegação na parte de baixo da tela que permite que o usuário alterne entre telas diferentes.
Primeiro, implemente esse elemento de navegação na parte de baixo da tela e inclua essa função no app.
Vamos observar o design:
Felizmente, não é necessário implementar essa função de composição inteira do zero. Você pode usar o BottomNavigation
, que faz parte da biblioteca Compose Material. No BottomNavigation
, é possível adicionar um ou mais elementos BottomNavigationItem
, 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.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
BottomNavigation(modifier) {
BottomNavigationItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
BottomNavigationItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
A implementação básica vai ficar assim:
Precisamos fazer algumas adaptações de estilo. Primeiro, defina o parâmetro backgroundColor
para atualizar a cor do plano de fundo da navegação. Você pode usar a mesma cor 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 à cor do onBackground
do tema. A solução final vai ficar assim:
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
BottomNavigation(
backgroundColor = MaterialTheme.colors.background,
modifier = modifier
) {
BottomNavigationItem(
icon = {
Icon(
imageVector = Icons.Default.Spa,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_home))
},
selected = true,
onClick = {}
)
BottomNavigationItem(
icon = {
Icon(
imageVector = Icons.Default.AccountCircle,
contentDescription = null
)
},
label = {
Text(stringResource(R.string.bottom_navigation_profile))
},
selected = false,
onClick = {}
)
}
}
12. App MySoothe: scaffold
Nesta etapa final, vamos criar a implementação em tela cheia e incluir a 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 de composição de navegação criado na etapa anterior nessa barra.
Implemente o elemento combinável MySootheApp. 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.material.Scaffold
@Composable
fun MySootheApp() {
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, faça o download da imagem abaixo e a compare com sua própria visualização.
13. 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 e componentes do Material Design.
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: