Android e Jetpack Compose: crie menus flexíveis com Slot API
Resumo
Neste artigo, vamos ver como construir um menu superior de uma aplicação Android de duas formas:
- Com o sistema de Views: a maneira antiga do Android, mais inflexível e trabalhosa;
- Com o Slot API: uma nova opção do Jetpack Compose, mais flexível e customizável.
Também, vamos usar esse padrão em outras partes do aplicativo e, temos até mesmo um desafio para você aprofundar seus estudos!
Vamos lá?
Introdução
Durante o processo de criação de componentes de UI (Interface gráfica), nos deparamos diversas vezes com implementações complicadas - e dentro do Android não seria diferente. Imagine que você, como dev Android, recebeu a tarefa de criar essa barra superior no seu layout:
E então? Há duas possibilidades iniciais:
- A primeira opção seria utilizar os próprios componentes do sistema de Views, por exemplo, a ActionBar ou a ToolBar;
- A segunda alternativa seria fazer o menu através dos Layouts.
Porém, um terceiro jeito de construir a barra superior seria com o Jetpack Compose, que facilita a criação de componentes semelhantes a esse.
Assim, em um primeiro momento, vamos analisar como é feita uma configuração simples de ActionBar
(pelo sistema de Views). Em seguida, veremos um jeito de fazer essa implementação com o Compose!
Criando menu na Action Bar (Sistema de Views)
Para saber como criar um menu na Action Bar, no antigo sistema de Views, veja os passos a seguir. Indicamos que você abra um projeto Android vazio para aprender com a gente!
Criando um arquivo de menu
Na pasta do seu projeto Android:
- Clique com o botão direito na pasta res;
- Vá em New, depois em Android Resource Directory ;
- Agora, em Resource type, selecione menu e dê um OK.
Legal! Agora uma pasta será criada, e ela vai ser responsável pelos recursos de menu. Continuando:
- Clique com o botão direito do mouse, selecione New > Menu Resource File, dê o nome que preferir ao arquivo; aqui vamos utilizar action_bar.
Configurando o menu
Dentro do arquivo action_bar.xml, coloque um único item que representará um logout no nosso app, e o código do arquivo ficaria dessa forma:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:icon="@drawable/ic_logout_white"
android:title="Sair do App"
app:showAsAction="ifRoom"
/>
</menu>
Configurando a ActionBar
Agora, na nossa Activity principal, crie um método que se encarregará de configurar esse menu:
private fun configuraActionBar() {
addMenuProvider(object : MenuProvider{
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.action_bar, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return true
}
})
}
Chamando a função no onCreate
Agora que fizemos o método configuraActionBar
, que é encarregado de colocar nosso menu na action bar, precisaremos chamá-lo dentro do nosso método onCreate
e, aí sim, teremos o resultado esperado na nossa Activity
:
…
override fun onCreate(savedInstanceState: Bundle?) {
…
configuraActionBar()
}
Esse botão não efetua nenhuma ação. Para ele fazer alguma coisa, precisaríamos de outras configurações. E se a gente quiser deixar mais opções nesse menu, bastaria colocar mais itens no nosso arquivo action_bar.xml
Pronto! Temos a barra superior! No entanto, e se quisermos centralizar o nome do aplicativo? Ou talvez posicionar de uma forma diferente esses itens? Nesse sentido, o sistema de Views é um pouco mais inflexível para fazer essas personalizações.
Para resolver isso, temos uma solução bem prática dentro do Jetpack Compose, pois customizações assim são bastante usadas em diversas aplicações e, neste artigo, também vamos abordar a Slot API - que é uma forma de facilitar a criação desses componentes.
Criando o menu com o Slot API do Jetpack Compose
Vamos agora aprender uma outra forma de construir o menu superior com o Slot API. Mas, você pode se perguntar: o que é isso?
O que é Slot API?
Basicamente, o Slot API é um composable, ou seja, uma “ferramenta” do Jetpack Compose que nos permite construir aplicativos Android de forma mais customizada. E ele tem uma característica muito legal: ele pode receber outros composables (veremos mais sobre isso a frente).
De forma mais técnica, o Slot API é um padrão para adicionar mais poder de personalização dentro dos componentes do Material Design como, por exemplo: Navigation Drawer, Floating Action Button, TopAppBar e etc.
Se você for mais iniciante, pode se perguntar: como assim o Slot API é um padrão? É por que o Slot API possui algumas características pré-definidas; ele funciona como uma forma, um “código pronto” que pode ser replicado em vários aplicativos diferentes para definir espaços dentro do composable.
Esse padrão traz flexibilidade através da capacidade de configurarmos um componente filho para um determinado ”setor” (ou espaço, slot). Podemos dizer que esse setor é como uma “caixa” que vai receber o conteúdo (um título da barra, um ícone etc).
A imagem abaixo apresenta como esses setores podem ser distribuídos; neste caso em específico, são três ícones com um título abaixo.
Layouts divididos em setor
Dentro de alguns componentes (e talvez da maioria das implementações do Material Design), conseguimos utilizar a Slot API através de setores dentro do próprio componente, onde cada um deles pode suportar outros composables. O que isso significa?
Bom, quer dizer que há partes do componente em que passamos um composable como argumento e ele vai se encaixar no setor que escolhermos, tornando assim uma ferramenta poderosa na personalização, dando mais flexibilidade em algumas implementações sem esse padrão. Vamos analisar um exemplo:
Acima, temos uma TopAppBar, esses setores suportam, por exemplo, um título ao meio e um menu à direita ou qualquer outro tipo de implementação que preferir, com cada uma delas tendo sua própria personalização. Podemos “modelar” o layout de forma muito mais simples e customizada.
Incrível, não é mesmo?
Como utilizar o Slot API para criar um menu
Para utilizarmos esse padrão do Slot API, vamos criar um projeto do zero.
Se é sua primeira vez com o Compose, recomendo que assista este alura+ Jetpack Compose: Começando com Compose | #AluraMais.
Criando o composable inicial
Para começar, crie um composable que representará o app dentro da MainActivity
:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App()
}
}
}
@Composable
fun App() {
ArtigoSlotAPITheme {
Surface {
}
}
}
Composable para a tela principal
Agora, crie um composable para a nossa tela principal (HomeScreen):
@Composable
fun HomeScreen() {
}
Em seguida, chame a HomeScreen
no nosso composable App
:
@Composable
fun App() {
ArtigoSlotAPITheme {
Surface {
HomeScreen()
}
}
}
Pronto! Finalizamos a estrutura básica a partir da qual vamos montar o menu superior.
Utilizando o Scaffold
Vamos utilizar um componente do Material Design chamado Scaffold (um ótimo ponto de partida para entendermos e utilizarmos a Slot API), pois ele conta com alguns setores já nomeados. Nesse exemplo, usaremos os Slots topBar
, que representa a barra superior do nosso app, e o content
, que seria a construção da nossa tela.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen() {
Scaffold(
topBar = { ScaffoldTopBar() },
){
Box(
modifier = Modifier.padding(it)
) {
ScaffoldImage()
}
}
}
No código acima, temos o ScaffoldTopBar
e o ScaffoldImage
, dois composables que vamos criar e personalizar.
Personalizando o Scaffold
Seguindo adiante, vamos criar e personalizar esses composables que representam nossa topbar e nosso conteúdo.
Sinta-se livre para fazer suas próprias customizações.
Um código possível seria o seguinte:
@Composable
fun ScaffoldTopBar() {
Row(
modifier = Modifier
.background(Purple700)
.padding(12.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Icon(
Icons.Rounded.Home,
contentDescription = null,
tint = Color.White
)
Text(
text = "Alura",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 22.sp
)
Icon(
Icons.Rounded.ExitToApp,
contentDescription = null,
tint = Color.White
)
}
}
O código acima resultará em algo semelhante a isto:
Em seguida, vamos implementar o ScaffoldImage
, que sugestivamente se tratará de uma imagem qualquer.
@Composable
fun ScaffoldImage() {
Image(
painter = painterResource(id = R.drawable.churrasco),
contentDescription = null
)
}
Resultado da implementação do Slot API
Se os passos foram executados corretamente, ao iniciarmos nosso app, a tela apresentará os componentes dispostos conforme a imagem abaixo:
Incentivamos que você faça as suas personalizações e seus testes, para que fique ainda mais claro como esse padrão do Slot API facilita a personalização dos nossos componentes.
Criando seu próprio Slot
Aprendemos como utilizar um componente que já usa o padrão Slot, certo?
Existe uma outra possibilidade: é possível criar os nossos próprios Slots, ou seja, teremos mais flexibilidade ao lidar com layouts que se repetem.
Mais a frente, teremos um desafio de implementação. Para fazer isso, nós vamos utilizar o projeto final do curso Jetpack Compose: utilizando Lazy Layout e estados aqui da Alura. Você consegue o código do projeto finalizado acessando o repositório do projeto, e usaremos especificamente o código da última aula com o projeto já finalizado, que você pode acessar aqui: Projeto final do curso.
Com o projeto em mãos, vamos começar!
Analisando o projeto
Dentro deste app, temos uma barra de pesquisa e diversas listas que têm linhas de produtos com um título.
Ao entrarmos no arquivo HomeScreen.kt
e, depois, em ui > screens, as sessões estão sendo iteradas e têm sempre o mesmo padrão de título em cima e uma lista disposta horizontalmente de algum conteúdo, vamos usar o padrão Slot nesta sessão.
Criando o componente com Slot
Neste exemplo, usaremos o ProductsSection
! Vamos começar entendendo sua estrutura e como o conteúdo está disposto.
Começamos com um Column
que possui um Text
e uma LazyRow
;
- Crie um arquivo kotlin dentro da pasta components;
- Coloque o nome de
Section
; - Replique essa organização nesse arquivo.
Antes de continuar, é interessante analisar a proposta do Slot, em que recebemos um composable no parâmetro da função. Uma forma de fazermos isso seria essa:
...
@Composable
fun Section(
title: @Composable () -> Unit
) ...
Aqui estamos solicitando um parâmetro nomeado de título, em que o tipo dele é uma função com a anotação @Composable
, em outras palavras, é um composable pedindo outro como argumento.
Vamos aplicar a mesma lógica para a LazyRow
que seria a parte do nosso conteúdo:
...
@Composable
fun Section(
title: @Composable () -> Unit,
content: @Composable () -> Unit
) ...
Agora, podemos fazer a distribuição dos parâmetros que recebemos:
...
@Composable
fun Section(
title: @Composable () -> Unit,
content: @Composable () -> Unit
) {
Column(modifier) {
title()
content()
}
}
E pronto, temos nosso composable com Slot criado, mas para deixar ele ainda melhor temos que acrescentar algumas coisas. Vamos lá?
Melhorando a implementação do slot
Sim! Podemos melhorar a implementação!
Vamos começar aumentando a capacidade de personalização do nosso Column
. Primeiro, passe um Modifier
como parâmetro e o aplique:
...
fun Section(
title: @Composable () -> Unit,
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier) {
...
}
}
Isso permite que a pessoa que for utilizar o nosso Section
possa personalizar ainda mais a implementação.
Segundo, coloque um Spacer
de 16.dp
entre o title
e o content
para que o layout fique um pouco mais agradável e menos aglomerado:
...
Column(modifier) {
title()
Spacer(modifier = Modifier.height(16.dp))
content()
}
...
E, assim, finalizamos a criação do nosso próprio Slot!
Refatorando o Projeto
A partir deste momento, vamos refatorar o código do projeto para um Slot.
De início, acesse o componente Products Section
. Chame o componente Section
dentro de ProductsSection
e siga os seguintes passos:
- Coloque o componente
Text
dentro do parâmetrotitle
e oLazyRow
dentro de content; - Passe o
modifier
que estamos recebendo doProductsSection
para oSection
; - Dentro de
LazyRow
terá um padding top de 8.dp que pode ser removido, pois no nossoSection
já possuímos umSpacer
que cumpre o mesmo papel.
O código final ficaria semelhante a este:
@Composable
fun ProductsSection(
title: String,
products: List<Product>,
modifier: Modifier = Modifier
) {
Section(
title = {
Text(
text = title,
Modifier.padding(
start = 16.dp,
end = 16.dp
),
fontSize = 20.sp,
fontWeight = FontWeight(400)
)
},
content = {
LazyRow(
Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
items(products) { p ->
ProductItem(product = p)
}
}
},
modifier = modifier
)
}
Não haverá mudanças no visual do seu app, porém, encorajamos que você faça alterações e explore esse padrão, pois ele fará com que a refatoração e a reutilização do seu código fiquem bem mais simples e sugestiva.
Desafio
Aprendemos como utilizar o Slot e quais são suas principais vantagens. E agora vamos a um breve desafio de implementação.
Vamos trazer uma situação da vida real: você é a pessoa desenvolvedora responsável por manter este projeto, e eu sou seu cliente que trouxe uma solicitação: é preciso que você faça mais uma seção no layout. Só que, ao invés de produtos, é necessário que apareça o título “lojas parceiras''. Assim, a pessoa encarregada do Design envia essas informações:
Especificações:
Card
- Fundo: branco
- Altura mínima / máxima: 150 dp e 200 dp
Imagem
- Tamanho: 100 dp
- Forma: Circular
Texto
- Tamanho da fonte: 16 sp
- Máximo de linhas: 2
- Alinhamento: central
- Padding: 8dp
Sinta-se à vontade para tentar realizar a sua solução com tudo que viu aqui no artigo.
Implementando o desafio
Neste momento, é importante ressaltar que essa é uma de várias possíveis soluções; você pode fazer do jeito que achar melhor, não precisando seguir a risca tudo que é passado.
Criando o Modelo de Dados
Começamos com a criação de um model Shop.kt
, que é uma data class, e vai levar as informações de nome da loja e também sua logo:
data class Shop(
val name: String,
val logo: String
)
Base de dados
Agora vamos precisar de uma lista e de um map, semelhante ao que conseguimos encontrar no arquivo SampleData.kt
, porém, agora é para nosso modelo de lojas.
Deixei alguns dados prontos, basta copiar e colar ao fim do arquivo:
val sampleShops: List<Shop> = listOf(
Shop(
name = "Carrinho SuperMercado",
logo = "https://images.pexels.com/photos/264547/pexels-photo-264547.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
),
Shop(
name = "Padaria",
logo = "https://images.pexels.com/photos/1855214/pexels-photo-1855214.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
),
Shop(
name = "Floricultura",
logo = "https://images.pexels.com/photos/2111192/pexels-photo-2111192.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
),
Shop(
name = "Loja de Roupas",
logo = "https://images.pexels.com/photos/102129/pexels-photo-102129.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
),
Shop(
name = "Hotéis",
logo = "https://images.pexels.com/photos/237272/pexels-photo-237272.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
),
)
val sampleShopSections = mapOf(
"Lojas Parceiras" to sampleShops
)
Implementando o Card da Loja
Para esse passo, nós temos a liberdade de personalizar do jeito que acharmos mais agradável. Neste caso, usaremos nosso Slot criado anteriormente para facilitar a implementação desse novo componente.
@Composable
fun Partner(
shop: Shop,
modifier: Modifier = Modifier,
) {
Surface(
modifier,
) {
Column(
Modifier
.heightIn(150.dp, 200.dp)
.width(100.dp)
) {
val imageSize = 100.dp
AsyncImage(
model = shop.logo,
contentDescription = null,
Modifier
.size(imageSize)
.clip(shape = CircleShape),
contentScale = ContentScale.Crop,
placeholder = painterResource(id = R.drawable.placeholder)
)
Text(
text = shop.name,
fontSize = 16.sp,
maxLines = 2,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
)
}
}
}
Criando a seção de lojas parceiras
Agora vamos fazer algo parecido com o ProductsSection
, onde faremos uma sessão de lojas, criando um componente com um título e uma linha para exibir os conteúdos.
@Composable
fun PartnersSection(
title: String,
shop: List<Shop>,
modifier: Modifier = Modifier
) {
Section(
title = {
Text(
text = title,
Modifier.padding(
start = 16.dp,
end = 16.dp
),
fontSize = 20.sp,
fontWeight = FontWeight(400),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
content = {
LazyRow(
Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
items(shop) { s ->
Partner(shop = s)
}
}
},
modifier = modifier
)
}
Finalizando
Essa é a última etapa do nosso desafio, precisaremos popular e colocar essa sessão dentro da nossa HomeScreen
:
@Composable
fun HomeScreen(
...
) {
Column {
...
LazyColumn(
...
) {
if (text.isBlank()) {
...
for (shopSections in sampleShopSections){
val title = shopSections.key
val shop = shopSections.value
item {
PartnersSection(title = title, shop = shop)
}
}
} else {
...
}
}
}
}
Com isso, temos o resultado desejado. Recomendamos que você teste e faça alterações usando o Slot API para que possa se familiarizar cada vez mais com essa ferramenta.
Conclusão
Aprendemos a utilizar a Slot API e identificar quais suas principais vantagens, trazendo diversas possibilidades de personalização dentro do Jetpack Compose. Agora, é possível deixar o nosso app Android da forma que preferimos.
Ainda existem diversas outras implementações que você pode acessar na documentação do próprio android sobre layouts em Slots.
Quer construir aplicativos Android incríveis e aprender mais sobre o assunto? Conheça a formação Jetpack Compose da Alura e bons estudos!