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)