Como implementar uma tela de carregamento no Jetpack Compose

alexfelipe

Alex Felipe

Posted on June 23, 2023

Como implementar uma tela de carregamento no Jetpack Compose

Ao desenvolver um App Android, é muito comum buscarmos informações para apresentar na tela, seja a busca de um banco de dados interno ou por uma API.

Independente da fonte de dados que usamos, uma coisa é certa, a disponibilidade dos dados nem sempre será garantida! Ou seja, enquanto o dado não está pronto, o que podemos mostrar para o nosso usuário?

Uma das abordagens comuns é apresentar uma tela ou um componente de carregamento. Existem várias maneiras de representar o carregamento, pode ser apenas um texto com a mensagem 'carregando', ou então, indicadores de progresso do Material Design.


TL;DR

Para você que quer apenas o código completo para testar e chegar neste resultado:

App em execução apresentando o indicador de progresso circular. Após 3 segundos, apresenta a lista de produtos com os produtos de amostra

A tela é representada por esse composable:

@Composable
fun ProductsListScreen(uiState: ProductsListUiState) {
    val products = uiState.products
    val isLoading = uiState.isLoading
    if (isLoading) {
        Box(modifier = Modifier.fillMaxSize()) {
            CircularProgressIndicator(Modifier.align(Center))
        }
    } else {
        LazyColumn(Modifier.fillMaxSize()) {
            items(products) { p ->
                Column(
                    Modifier
                        .clip(RoundedCornerShape(10.dp))
                        .padding(8.dp)
                        .fillMaxWidth()
                        .border(
                            1.dp,
                            Color.Gray.copy(alpha = 0.5f),
                            RoundedCornerShape(10.dp)
                        )
                        .padding(8.dp)
                ) {
                    Text(text = p.name, fontWeight = FontWeight.Bold, fontSize = 24.sp)
                    Text(text = p.description)
                    Text(
                        text = p.price.toBrazilianCurrency(),
                        fontWeight = FontWeight.Bold,
                        style = TextStyle.Default.copy(color = Color(0xFF4CAF50)),
                        fontSize = 18.sp
                    )
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

o modelo de produto, lista de produtos de amostra e o UiState:

class ProductsListUiState(
    val products: List<Product> = emptyList(),
    val isLoading: Boolean = true
)

val sampleProducts = List(10) {
    Product(
        name = LoremIpsum(Random.nextInt(1, 5)).values.first(),
        description = LoremIpsum(Random.nextInt(1, 10)).values.first(),
        price = BigDecimal(Random.nextInt(1, 100) * it)
    )
}

class Product(
    val name: String,
    val description: "String,"
    val price: BigDecimal
)
Enter fullscreen mode Exit fullscreen mode

O formatador de moeda brasileira:

private fun BigDecimal.toBrazilianCurrency(): String =
    NumberFormat.getCurrencyInstance(
        Locale("pt", "br")
    ).format(this)
Enter fullscreen mode Exit fullscreen mode

o código para o gerenciamento de estado com o UiState:

var uiState by remember {
    mutableStateOf(ProductsListUiState())
}
LaunchedEffect(null) {
    delay(3000)
    uiState = ProductsListUiState(
        products = sampleProducts,
        isLoading = false
    )
}
ProductsListScreen(uiState)
Enter fullscreen mode Exit fullscreen mode

Caso o seu interesse é entender como esse código funciona e as motivações para implementá-lo dessa forma, continue a leitura do artigo 😄


Como representar um carregamento na tela?

No Jetpack Compose, podemos apresentar um indicador de progresso circular da seguinte maneira:

Box(modifier = Modifier.fillMaxSize()) {
    CircularProgressIndicator(
        Modifier.align(Alignment.Center)
    )
}
Enter fullscreen mode Exit fullscreen mode

App em execução apresentando um indicador de progresso circular indeterminado

Observe que temos um indicador indeterminado, ou seja, ele fica girante pra sempre! Novamente, essa é apenas uma possibilidade de representação e poderíamos usar qualquer outro componente... O grande detalhe é:

"Como podemos escrever uma lógica para apresentar ou esconder o carregamento no momento esperado?" 🤔

Gerenciamento de estado

Para isso, precisamos utilizar gerenciamento de estado no Jetpack Compose. Há mais de uma implementação de gerenciamento de estado, seja direto nos composables ou a partir de ViewModel que é o mais recomendado.

Dado que o objetivo deste artigo é focar em mostrar apenas como fazer a representação de carregamento, farei a implementação direta no composable. Antes de mexer no código, vamos entender o que precisamos fazer:

  • tela principal que precisa buscar os produtos
    • ao acessar a tela, apresenta uma representação de carregamento
    • após carregar produtos, mostra os produtos em lista

Agora que sabemos o que precisamos implementar, vamos seguir com o código.

Definindo o modelo e criando amostras de produtos

Primeiro, vamos começar com o modelo para o produto e a criação de algumas amostras:

class Product(
    val name: String,
    val description: String,
    val price: BigDecimal
)

val sampleProducts = List(10) {
    Product(
        name = LoremIpsum(Random.nextInt(1, 5)).values.first(),
        description = LoremIpsum(Random.nextInt(1, 10)).values.first(),
        price = BigDecimal(Random.nextInt(1, 100) * it)
    )
}
Enter fullscreen mode Exit fullscreen mode

Com esse código, geramos 10 produtos com valores aleatórios para nome, descrição e preço.

Implementação da tela da lista de produtos

Para apresentar os produtos, vamos criar a tela de lista de produtos:

@Composable
fun ProductsListScreen(products: List<Product>) {
    LazyColumn(Modifier.fillMaxSize()) {
        items(products) { p ->
            Column(
                Modifier
                    .clip(RoundedCornerShape(10.dp))
                    .padding(8.dp)
                    .fillMaxWidth()
                    .border(
                        1.dp,
                        Color.Gray.copy(alpha = 0.5f),
                        RoundedCornerShape(10.dp)
                    )
                    .padding(8.dp)
            ) {
                Text(text = p.name, fontWeight = FontWeight.Bold, fontSize = 24.sp)
                Text(text = p.description)
                Text(
                    text = p.price.toBrazilianCurrency(),
                    fontWeight = FontWeight.Bold,
                    style = TextStyle.Default.copy(color = Color(0xFF4CAF50)),
                    fontSize = 18.sp
                )
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

E para melhorar o visual do preço, vamos também adicionar o formatador de moeda brasileira:

private fun BigDecimal.toBrazilianCurrency(): String =
    NumberFormat.getCurrencyInstance(
        Locale("pt", "br")
    ).format(this)
Enter fullscreen mode Exit fullscreen mode

App em execução apresentado uma lista de produtos, com nome, descrição e preço.

Com a tela implementada, podemos focar no gerenciamento de estado da mesma.

Representando o estado da interface de usuário - UI State

Para representar os dados da tela, utilizamos o padrão de estado de interface do usuário ou UI State. A ideia de um UI State é conter os dados que devem ser apresentados na tela, ou então, um estado do dado, como por exemplo, um estado que indique se o dado foi carregado, se houve falha ou se está carregando!

Essa representação pode ser feita a partir de uma classe qualquer:

class ProductsListUiState(
    val products: List<Product> = emptyList(),
    val isLoading: Boolean = true
)
Enter fullscreen mode Exit fullscreen mode

E então, a nossa tela pode receber ProductsListUiState via parâmetro e apresentar o conteúdo com base nos dados do UI State:

@Composable
fun ProductsListScreen(uiState: ProductsListUiState) {
    val products = uiState.products
    val isLoading = uiState.isLoading
    if (isLoading) {
        Box(modifier = Modifier.fillMaxSize()) {
            CircularProgressIndicator(Modifier.align(Center))
        }
    } else {
        LazyColumn(Modifier.fillMaxSize()) { ... }
    }
}
Enter fullscreen mode Exit fullscreen mode

Nesta configuração, enquanto o isLoading estiver como true, a lista não é apresentada!

Testando o App simulando a busca de produtos

Agora que temos a representação, precisamos modificar a lógica de chamada para que faça a busca das informações e atualize o UI State:

var uiState by remember {
    mutableStateOf(ProductsListUiState())
}
LaunchedEffect(null) {
    delay(3000)
    uiState = ProductsListUiState(
        products = sampleProducts,
        isLoading = false
    )
}
ProductsListScreen(uiState)
Enter fullscreen mode Exit fullscreen mode

App em execução apresentando o indicador de progresso circular. Após 3 segundos, apresenta a lista de produtos com os produtos de amostra

Veja que agora o App apresenta o indicador de progresso circular, e então, após 3 segundos, mostra a lista de produtos! Não entendeu o código? Vamos para uma breve explicação.

  1. Definimos o UI State a partir de um remember para que a inicialização não seja afetada pela recomposição
  2. o LaunchedEffect(null) é um composable de Side Effect que não emite componente visual.
  3. Componentes de Side Effect dão suporte para códigos que não são afetados pela recomposição. Ao enviar null como argumento, indicamos que ele vai executar apenas uma vez!
  4. O LaunchedEffect() também permite executar coroutines, e é por isso que ele não trava a execução mesmo aplicando um delay de segundos via coroutine.
  5. Ao modificar o uiState acontece a recomposição e é enviado um novo uiState com os produtos novos e a sinalização de que não está mais carregando para a tela de lista de produtos.

Para saber mais

É importante ressaltar que essa é uma implementação simples e objetiva, ou seja, existem abordagens mais sofisticadas e recomendadas para implementar o gerenciamento de estado com UI State. Um dos exemplos mais usados, é a partir de ViewModels, embora seja mais complexo ele resolve uma série de detalhes a nível de arquitetura de App.

Se você tem interesse no assunto e quer aprender a partir de um conteúdo mais estruturado, posso te recomendar a formação de Jetpack Compose: gerenciamento de estado da Alura. Caso você ainda não seja assinante, eu posso te ajudar com um cupom de desconto da assinatura da Alura.

O que você achou desta implementação? Já conhecia essas técnicas de gerenciamento de estado para apresentar um conteúdo dinâmico nos composables? Aproveite para compartilhar outras técnicas que utiliza pra isso 😉

💖 💪 🙅 🚩
alexfelipe
Alex Felipe

Posted on June 23, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related