Android e Jetpack Compose: crie menus flexíveis com Slot API

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:

  1. Com o sistema de Views: a maneira antiga do Android, mais inflexível e trabalhosa;
  2. 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á?

Banner da Escola de Mobile: Matricula-se na escola de Mobile. Junte-se a uma comunidade de mais de 500 mil estudantes. Na Alura você tem acesso a todos os cursos em uma única assinatura; tem novos lançamentos a cada semana; desafios práticos. Clique e saiba mais!

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:

“Barra superior de um aplicativo de smartphone com o título ‘Material 3’, ao lado direito um ícone de lupa e 3 pontos vertical indicando um menu, abaixo temos um texto descrevendo o material 3’”.

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

    }
“Aplicativo de celular com fundo branco, uma barra superior em cor roxa, com o texto ‘Alura’ em branco, à direita da barra superior temos um ícone de uma flecha apontando para o lado direito de dentro para fora de um retângulo distribuído verticalmente também na cor branca. O Restante da aplicação está em um fundo branco com um texto escrito: ‘Hello World’ em cinza claro bem ao centro da tela“

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.

“Imagem com fundo branco, ao lado esquerdo temos o ícone de um avião em roxo com um texto em baixo escrito ‘Flights’, há uma ligação do número um e onúmero cinco a este ícone. Ao centro temos o ícone de uma mala de viagem com um título em baixo: ‘Trips’, o ícone e o texto estão na cor cinza escuro e está contornado com uma linha tracejada em cinza escuro, há também um número dois ligado acima do ícone. À direita, há o ícone de uma bússola com o título em baixo: ‘Explore’, o ícone e o texto estão na cor cinza escuro, estão com o número quatro e o número três ligado a ele.”

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:

“Barra superior de um aplicativo de smartphone, com a cor roxa, há três setores contornados com uma linha tracejada em branco dividindo a barra em seu interior.”

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

“Barra superior de um aplicativo de smartphone, com a cor de fundo em roxo, ao lado esquerdo dela há um ícone de uma casa na cor branca, ao centro temos um texto descrito: ‘Alura’ em branco e à direita um ícone de um contorno quadrado com uma seta entrando pelo lado esquerdo do quadrado e apontando para o lado direito também em branco”

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:

“Imagem de um aplicativo de smartphone ao topo temos uma barra superior, com a cor de fundo em roxo, ao lado esquerdo dela há um ícone de uma casa na cor branca, ao centro temos um texto descrito: ‘Alura’ em branco e à direita um ícone de um contorno quadrado com uma seta entrando pelo lado esquerdo do quadrado e apontando para o lado direito também em branco. Abaixo desta barra, temos uma imagem de uma grade de churrasco, em cima dela temos alguns pedaços de carne sendo assados e fogo subindo ao meio desta grade, a imagem se prolonga até metade da tela do smartphone, onde o resto é um fundo branco”

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.

“Gif do Aplicativo Aluvery, realizado no curso de jetpack compose lazy layouts e estados na Alura, apresenta um fundo branco, com uma barra de pesquisa arredondada nas extremidades laterais, com uma lupa ao lado esquerdo na cor cinza claro. Abaixo temos sessões de Promoções, Doces e Sucos, essas sessões possuem cards distribuídos horizontalmente que podem ser roláveis. Estes cards são retangulares com os cantos arredondados, possuindo um fundo com degradê de lilás para o azul claro até ocupar cerca de 40% a altura do card o restante do fundo é branco, ao meio do card temos uma imagem redonda que representa o produto que fica ao meio entre o fundo lilás e o fundo branco, abaixo da imagem temos um texto representando qual é o produto e mais abaixo temos outro texto com seu preço. Ao usar a barra de pesquisa o layout é alterado para uma lista vertical dos cards que tem relação ao texto pesquisado”.

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âmetro title e o LazyRow dentro de content;
  • Passe o modifier que estamos recebendo do ProductsSection para o Section;
  • Dentro de LazyRow terá um padding top de 8.dp que pode ser removido, pois no nosso Section já possuímos um Spacer 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:

“Gif do Aplicativo Aluvery, realizado no curso de jetpack compose lazy layouts e estados na Alura, apresenta um fundo branco, com uma barra de pesquisa arredondada nas extremidades laterais, com uma lupa ao lado esquerdo na cor cinza claro. Abaixo temos sessões de Promoções, Doces e Sucos, essas sessões possuem cards distribuídos horizontalmente que podem ser roláveis. Estes cards são retangulares com os cantos arredondados, possuindo um fundo com degradê de lilás para o azul claro até ocupar cerca de 40% a altura do card o restante do fundo é branco, ao meio do card temos uma imagem redonda que representa o produto que fica ao meio entre o fundo lilás e o fundo branco, abaixo da imagem temos um texto representando qual é o produto e mais abaixo temos outro texto com seu preço. Mais abaixo temos outra sessão que representa as lojas parceiras, onde há uma imagem redonda com o nome da loja abaixo”

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!

Matheus Perez
Matheus Perez

Matheus é estudante de ciências da computação, apaixonado por tecnologia em geral, desenvolvedor mobile nativo (Android/iOS) com as linguagens Kotlin e Swift, entusiasta nas áreas de UX/UI. Seu principal foco é trazer ótimas experiências com tecnologia.

Veja outros artigos sobre Mobile