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:
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 } }
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 ) }
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 ) } } } }
Caso você queira ver o formatador de moeda também:
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:
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 ) }
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)
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)) ) } } }
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 } }
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 ) }
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 😄
Top comments (0)