Como implementar uma busca com filtro no Jetpack Compose

alexfelipe

Alex Felipe

Posted on July 4, 2023

Como implementar uma busca com filtro no Jetpack Compose

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

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

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
    }

}
Enter fullscreen mode Exit fullscreen mode

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

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
    )
}
Enter fullscreen mode Exit fullscreen mode

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

@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
                )
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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:

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
    )
}
Enter fullscreen mode Exit fullscreen mode

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

@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)
Enter fullscreen mode Exit fullscreen mode

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:

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

}
Enter fullscreen mode Exit fullscreen mode

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:

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
    }

}
Enter fullscreen mode Exit fullscreen mode

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:

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
    )
}
Enter fullscreen mode Exit fullscreen mode

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

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 😄

💖 💪 🙅 🚩
alexfelipe
Alex Felipe

Posted on July 4, 2023

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

Sign up to receive the latest update from our blog.

Related