Layouts básicos no Compose

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

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 das filhas no contêiner pai.
  • Como as funções do Material Design, por exemplo, o Scaffold e a navegação na parte de baixo da tela, ajudam a criar layouts abrangentes (link em inglês).
  • Como criar funções de composição flexíveis usando APIs de slot.

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:

24ff9efa75f22198.png

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:

c31e78e48cc1f336.png

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.

9a0f4be94a5a206c.png

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

d9bf2ca5a0939959.png

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.

29bed1f813622dc.png

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

cf1fe8b2d682bfca.png

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:

6b7c2f913d189b9a.png

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.

6c6854661a89e995.png

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

c2e1eec30f36bc72.png

Ainda faltam algumas coisas. Primeiro, vamos corrigir o tamanho da função de composição 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 função de composição tem um parâmetro modifier, que pode ser definido para adaptar a aparência e o comportamento dessa função. 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:

6b7c2f913d189b9a.png

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.
  • Defina a cor do plano de fundo do campo de texto como a cor surface do MaterialTheme. Você pode usar TextFieldDefaults.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 dados TextFieldDefaults 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 de composição e modificadores para mudar a aparência de uma função de composição. 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:

29bed1f813622dc.png 9d11e16a8817686f.png

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:

b9686f83eb73c542.png

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:

5f17f07fcd0f1dc.png

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:

6576ed1e8b1cde30.png

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:

71fcfc487ef8c02a.png

f2f4fb696389ba4f.png

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:

5584e459f9838f8b.png

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:

e0afeb1658a6d82a.png

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

25089e1f3e5eab4e.gif

Observe o esboço desse componente:

9d943fabcb5ae632.png

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 ver, as filhas de LazyRow não são funções de composição. 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:

b88f30efe9067f53.png

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:

c1e6c40e30136af2.gif

Já para uma Column:

df69881d07b064d0.gif

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:

c29a8ee73f218868.png

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:

6b3f390040e2b7fd.gif

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:

4378867d758590ae.gif

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

e51beb5c00457902.png

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:

2c0bc456d50bb078.png

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:

d824b60e650deeb.png

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() da String 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:

a535e10437e9df31.png

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)
   ) {
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
   }
}

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:

2f42ad2b882885f8.png

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:

5bdb7729d75e1a72.png

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 a função de composição 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 de composição 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.

24ff9efa75f22198.png

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: