Como implementar uma tela de carregamento no Jetpack Compose
Tags: kotlin, android, jetpackcompose
Published: 2023-06-23 11:00:00


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](https://m2.material.io/components/progress-indicators/android#circular-progress-indicators).

---
## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sdj1160hhd1k2gh32ks3.gif)

A tela é representada por esse composable:

```kotlin
@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
                    )
                }
            }
        }
    }
}
```

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

```kotlin
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
)
```

O formatador de moeda brasileira:

```kotlin
private fun BigDecimal.toBrazilianCurrency(): String =
    NumberFormat.getCurrencyInstance(
        Locale("pt", "br")
    ).format(this)
```

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

```kotlin
var uiState by remember {
    mutableStateOf(ProductsListUiState())
}
LaunchedEffect(null) {
    delay(3000)
    uiState = ProductsListUiState(
        products = sampleProducts,
        isLoading = false
    )
}
ProductsListScreen(uiState)
```

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:

```kotlin
Box(modifier = Modifier.fillMaxSize()) {
    CircularProgressIndicator(
        Modifier.align(Alignment.Center)
    )
}
```

![App em execução apresentando um indicador de progresso circular indeterminado](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0creian18fx1cibwh4rl.gif)

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](https://developer.android.com/jetpack/compose/state) no Jetpack Compose. Há mais de uma implementação de gerenciamento de estado, seja direto nos composables ou a partir de [ViewModel](https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state#0) 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:

```kotlin
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)
    )
}
```

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:

```kotlin
@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
                )
            }
        }
    }
}
```

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

```kotlin
private fun BigDecimal.toBrazilianCurrency(): String =
    NumberFormat.getCurrencyInstance(
        Locale("pt", "br")
    ).format(this)
```

![App em execução apresentado uma lista de produtos, com nome, descrição e preço.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ht0l330uep8tr7qcnmh8.png)

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](https://developer.android.com/topic/architecture/ui-layer/stateholders#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:

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

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

```kotlin
@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()) { ... }
    }
}
```

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:

```kotlin
var uiState by remember {
    mutableStateOf(ProductsListUiState())
}
LaunchedEffect(null) {
    delay(3000)
    uiState = ProductsListUiState(
        products = sampleProducts,
        isLoading = false
    )
}
ProductsListScreen(uiState)
```

![App em execução apresentando o indicador de progresso circular. Após 3 segundos, apresenta a lista de produtos com os produtos de amostra](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sdj1160hhd1k2gh32ks3.gif)

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](https://developer.android.com/jetpack/compose/side-effects) 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](https://developer.android.com/jetpack/compose/state-hoisting#viewmodels-as-state-owner), 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](https://www.alura.com.br/formacao-jetpack-compose-criando-telas-gerenciando-estados). Caso você ainda não seja assinante, eu posso te ajudar com um [cupom de desconto da assinatura da Alura](https://www.alura.com.br/promocao/alexfelipe).

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 😉