Como implementar uma busca com filtro no Jetpack Compose
Tags: kotlin, android, jetpackcompose
Published: 2023-07-04 11:00:00

Uma funcionalidade muito comum em diversos Apps, é permitir que os usuários façam buscas. A busca pode ser feita a partir de textos, categorias ou alguma informação que permita filtrar dados do App.

Dessa forma, melhoramos a experiência do usuário que tende a encontrar o que ele busca com mais facilidade, concorda? Agora vem a questão:

> _"Como podemos implementar uma busca com filtro no Jetpack Compose?"_

---

## TL;DR

Se o seu objetivo é verificar o código final sem entender as motivações, você pode visualizar abaixo:

![App em execução apresentando um campo de texto e uma lista de produtos em coluna. Ao digitar no campo de texto, aparece apenas os produtos que contém o nome ou descrição com o valor do campo de texto](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ablm1ggbjcoqr46pm16y.gif)

O mais importante de todos é o ViewModel que mantém a lógica para filtrar, nesse caso, filtrar produtos:

```kotlin
class ProductsListViewModel : ViewModel() {

    private val products = MutableStateFlow(emptyList<Product>())
    private val _filteredProducts = MutableStateFlow(emptyList<Product>())
    val filteredProducts = _filteredProducts.asStateFlow()

    fun searchProducts(text: String) {
        _filteredProducts.value = if (text.isEmpty()) {
            products.value
        } else {
            products.value.filter {
                it.name
                    .contains(
                        text,
                        ignoreCase = true
                    ) || it.description
                    .contains(
                        text,
                        ignoreCase = true
                    )
            }
        }

    }

    init {
        products.value = List(10) {
            Product(
                name = LoremIpsum(Random.nextInt(1, 10)).values.first(),
                description = LoremIpsum(Random.nextInt(1, 10)).values.first(),
                price = BigDecimal(Random.nextInt(10, 1000))
            )
        }
        _filteredProducts.value = products.value
    }

}
```

Então temos o código da tela para implementar o campo de texto e lista de produtos:

```kotlin
val viewModel by viewModels<ProductsListViewModel>()
val products by viewModel.filteredProducts.collectAsState(initial = emptyList())
Column {
    var searchText by remember {
        mutableStateOf("")
    }
    OutlinedTextField(
        value = searchText,
        onValueChange = {
            searchText = it
            viewModel.searchProducts(searchText)
        },
        Modifier
            .padding(8.dp)
            .fillMaxWidth(),
        label = {
            Text(text = "Buscar")
        },
        leadingIcon = {
            Icon(Icons.Default.Search, "search icon")
        },
        placeholder = {
            Text(text = "O que você procura?")
        },
        shape = RoundedCornerShape(10.dp)
    )
    ProductsListScreen(
        products = products
    )
}
```

E o código do composable que representa a lista de produtos:

```kotlin
@Composable
fun ProductsListScreen(
    products: List<Product> = emptyList()
) {
    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
                )
            }
        }
    }
}
```

Caso você queira ver o formatador de moeda também:

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

Agora, se a sua intenção é entender os passos para chegar nesse código, é só seguir com a leitura.

---

## Projeto de exemplo

Para exemplificar a implementação, vamos utilizar um App que tem um campo de texto e uma lista de produtos:

![App em execução apresentando um campo de texto e uma lista de produtos em coluna. Ao digitar no campo de texto, não modifica os items da lista de produtos. A lista de produtos é rolável com até 10 itens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/160vhtvuycjzpq4ba8sh.gif)

Um App simples para focar apenas na funcionalidade de filtrar os produtos.

## Código da tela

Se você quer replicar exatamente o mesmo resultado, você pode acessar o código da tela também:

```kotlin
Column {
    val products by remember {
        mutableStateOf(List(10) {
            Product(
                name = LoremIpsum(Random.nextInt(1, 10)).values.first(),
                description = LoremIpsum(Random.nextInt(1, 10)).values.first(),
                price = BigDecimal(Random.nextInt(10, 1000))
            )
        })
    }
    var searchText by remember {
        mutableStateOf("")
    }
    OutlinedTextField(
        value = searchText,
        onValueChange = {
            searchText = it
        },
        Modifier
            .padding(8.dp)
            .fillMaxWidth(),
        label = {
            Text(text = "Buscar")
        },
        leadingIcon = {
            Icon(Icons.Default.Search, "search icon")
        },
        placeholder = {
            Text(text = "O que você procura?")
        },
        shape = RoundedCornerShape(10.dp)
    )
    ProductsListScreen(
        products = products
    )
}
```

E aqui está o composable para representar a lista de produtos e o formatador de moeda:

```kotlin
@Composable
fun ProductsListScreen(
    products: List<Product> = emptyList()
) {
    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
                )
            }
        }
    }
}

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

Pronto! Isso é o suficiente para iniciarmos a implementação do filtro.

## ViewModel para buscar as informações da tela

A primeira coisa que precisamos pensar, é que o filtro trata-se de uma lógica de manipulação de dados, ou seja, o ideal é que essa lógica fique em algum outro lugar que não seja a tela.

Portanto, precisamos criar um ViewModel para manter essa lógica pra gente e ele pode começar contendo uma lista de produtos que vai representar a fonte de dados, ou seja, todos os produtos da tela:

```kotlin
class ProductsListViewModel : ViewModel() {

    private val products = MutableStateFlow(emptyList<Product>())

    init {
        products.value = List(10) {
            Product(
                name = LoremIpsum(Random.nextInt(1, 10)).values.first(),
                description = LoremIpsum(Random.nextInt(1, 10)).values.first(),
                price = BigDecimal(Random.nextInt(10, 1000))
            )
        }
    }

}
```

> Geralmente a fonte de dados é representada por um banco de dados ou comunicação via uma REST API.

A partir desse momento, temos tudo que precisamos para começar a manipulação dos dados.

## Adicionar os dados para representar o filtro

No caso do filtro, precisamos que exista uma outra lista para representar os produtos filtrados, afinal, a lista de produtos representa a fonte de dados e não deve ser modificada:

```kotlin
class ProductsListViewModel : ViewModel() {

    private val products = MutableStateFlow(emptyList<Product>())
    private val _filteredProducts = MutableStateFlow(emptyList<Product>())
    val filteredProducts = _filteredProducts.asStateFlow()

    fun searchProducts(text: String) {
        _filteredProducts.value = if (text.isEmpty()) {
            products.value
        } else {
            products.value.filter {
                it.name
                    .contains(
                        text,
                        ignoreCase = true
                    ) || it.description
                    .contains(
                        text,
                        ignoreCase = true
                    )
            }
        }

    }

    init {
        // ...
        _filteredProducts.value = products.value
    }

}
```

Se esse código pareceu complexo, vamos entender o que ele faz:

- `init`: inicializa as properties necessárias:
 - lista de produtos que vai representar a fonte de dados que não pode ser modificada
 - lista de produtos filtrados com o mesmo valor da fonte, pois no estado inicial (sem ter um texto para buscar), apresentam todos os produtos.
- `searchProducts()`: método para fazer a busca a partir de um texto:
 - primeiro verificamos se o valor do texto é ou não vazio, caso seja vazio, precisamos indicar que os produtos filtrados tenham o mesmo valor da fonte de dados, caso contrário, aplicamos a lógica de filtro.
 - O filtro é feite com o `filter()` de collection que permite adicionar condições, nesse caso, devolver apenas os produtos com nome ou descrição que contenham o texto recebido via parâmetro.
 - a fonte da busca sempre vai ser a fonte dos dados, pois, além de ter todos os produtos, nunca é alterada.
- a property `filteredProducts` é a única que deve ser pública para realizar a leitura na tela.

Agora que temos o código do ViewModel, é só conectar na tela.

## Realizando o filtro a partir do evento de mudança de texto

No código de tela, precisamos apenas criar o ViewModel, fazer a leitura dos produtos filtrados e chamar o método de busca de produtos no evento de mudança de texto:

```kotlin
val viewModel by viewModels<ProductsListViewModel>()
val products by viewModel.filteredProducts.collectAsState(initial = emptyList())
Column {
    var searchText by remember {
        mutableStateOf("")
    }
    OutlinedTextField(
        value = searchText,
        onValueChange = {
            searchText = it
            viewModel.searchProducts(it)
        },
        // ...
    )
    ProductsListScreen(
        products = products
    )
}
```

![App em execução apresentando um campo de texto e uma lista de produtos em coluna. Ao digitar no campo de texto, aparece apenas os produtos que contém o nome ou descrição com o valor do campo de texto](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ablm1ggbjcoqr46pm16y.gif)

Pronto! Implementamos um código para realizar filtros em um App com o Jetpack Compose. É válido ressaltar que essa foi uma implementação simples, mas podem haver mais etapas dependendo do escopo, como busca por diversas fontes, tratamentos etc.

O que você achou desta implementação? Faz de uma maneira diferente? Aproveite e deixe um comentário 😄