Layouts básicos no Compose

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

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:

Versão retrato do app

Versão paisagem do app

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:

design de retrato

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.

detalhamento do design do app

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).

detalhamento do design do app

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.

elemento "align your body"

  • O card "favorite collections", mostrado em uma grade rolável na horizontal.

card "favorite collection"

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:

barra de pesquisa

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.

esboço da barra de pesquisa

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âmetro modifier e o transmite ao TextField. 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:

visualização da barra de pesquisa

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:

barra de pesquisa

Siga estas etapas para atualizar a implementação:

  • Adicione o ícone de pesquisa. O TextField contém um parâmetro leadingIcon, que aceita outra função de composição. Dentro dele, defina um Icon, que, nesse caso, é o ícone Search. Use a importação Icon correta do Compose.
  • Use TextFieldDefaults.textFieldColors para substituir cores específicas. Defina a focusedContainerColor e a unfocusedContainerColor do campo de texto como a cor surface 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)
   )
}

barra de pesquisa

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 dados TextFieldDefaults e atualizar apenas as cores que são diferentes. Nesse caso, apenas as cores unfocusedContainerColor e focusedContainerColor 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:

componente "align your body"

esboço do elemento "align your body"

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:

visualização do elemento "align your body"

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:

visualização do elemento "align your body"

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:

visualização do conteúdo do elemento "align your body"

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:

visualização do elemento "align your body"

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.

esboço do elemento "align your body"

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".

visualização do elemento "align your body"

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:

card "favorite collection"

esboço do card "favorite collection"

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:

visualização do card "favorite collection"

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.

esboço do card "favorite collection"

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:

visualização do card "favorite collection"

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.

visualização do card "favorite collection"

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".

elemento rolável "align your body"

Observe o esboço desse componente:

esboço do elemento "align your body"

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:

visualização do elemento "align your body"

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:

disposição das linhas

Já para uma Column:

disposição das colunas

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:

visualização do elemento "align your body"

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:

esboço do elemento "align your body"

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.

elemento rolável "align your body"

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:

elemento rolável "favorite collections"

É 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:

visualização do elemento "favorite collections"

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:

visualização do elemento "favorite collections"

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:

esboço da seção da página inicial

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:

visualização da seção da página inicial

Ainda precisamos adicionar mais informações ao elemento de texto para que ele fique de acordo com o design.

esboço da seção da página inicial

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:

esboço da seção da página inicial

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() }
}

rolagem do conteúdo da tela

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:

design do elemento de navegação da parte de baixo

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:

visualização do elemento de navegação da parte de baixo

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:

design do elemento de navegação da parte de baixo

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:

implementação do MySoothe

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:

design de paisagem

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:

design do elemento de navegação da parte de baixo

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 = {}
           )
       }
   }
}

visualização da coluna de navegação

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 à cor onBackground 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 = {}
           )
       }
   }
}

design da coluna de navegação

Agora, vamos adicionar a coluna de navegação ao layout de paisagem:

design 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()
           }
       }
   }
}

visualização do modo paisagem

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á:

diagrama de tamanho de janela

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.

Versão retrato do app

Versão paisagem do app

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: