Solucionado (ver solução)
Solucionado
(ver solução)
4
respostas

(Resolvido) Desafio: campo de busca na tela inicial nem sempre busca certo

Bom dia a todos

Tentei fazer o desafio Desafio: campo de busca na tela inicial, fazendo a busca do meu jeito, realizando os métodos do Dao dentro do view Model e a lista ser atualizado de acordo com a mudança do texto. Até um certo nível deu certo. Mas o problema é que na hora de pesquisar, algumas vezes o resultado retorna vazio, mesmo que o texto seja igual ao pesquisado.

Acredito que seja pelo fato do view model pegar o resultado antes do dao realize a pesquisa. Código do view Model:

@HiltViewModel
class ListaContatosViewModel @Inject constructor(
    private val contatoDao: ContatoDao,
) : ViewModel() {

    private val _uiState = MutableStateFlow(ListaContatosUiState())
    val uiState: StateFlow<ListaContatosUiState>
        get() = _uiState.asStateFlow()

    init {
        viewModelScope.launch {
            _uiState.update { state ->
                state.copy(
                    alteracaoDoTextoDeBusca = {
                        _uiState.value = uiState.value.copy(
                            textoDeBusca = it,
                            // método de busca seja atualizado junto com o texto da barra de pesquisa
                            buscaDeContatos = buscaDeContatos(it)
                        )
                    }
                )
            }
            viewModelScope.launch {
                contatoDao.buscaTodos().collect {
                    _uiState.value = _uiState.value.copy(
                        contatos = it
                    )
                }
            }
        }
    }
// método da busca de contatos, deve ta mandando o empty antes da busca do dao ser feita 
    private fun buscaDeContatos(nome: String): List<Contato> {
        var contatosBuscados: List<Contato> = emptyList()
        viewModelScope.launch {
        // eu acho que eu precisaria garantir que isso seja buscado antes que o view model pegue o resultado
            contatosBuscados = contatoDao.buscaPorNome(nome).first()
        }
        return contatosBuscados
    }
}

Já tentei com outros métodos com o contatoDao.buscaPorNome(nome), já tentei o first(), o firstOrNull() , collect(). Também tentei o viewModelScope.async{} no lugar do viewModelScope.launch{} e mesmo assim as vezes dá certo, as vezes a lista volta nulo.

O que eu devo fazer para a busca ser feita e só depois ter o retorno da busca?

vou deixar o resto do código abaixo:

Código da Screen

@Composable
fun ListaContatosTela(
    state: ListaContatosUiState,
    modifier: Modifier = Modifier,
    onClickDesloga: () -> Unit = {},
    onClickAbreDetalhes: (Long) -> Unit = {},
    onClickAbreCadastro: () -> Unit = {},
) {
    Scaffold(
        topBar = { ...}
        }) { paddingValues ->
        Column {
            Spacer(modifier = Modifier.padding(paddingValues))
            Column (...){
                IconeDeBusca(state = state)
                val lista = if (state.textoDeBuscaEstaEmBranco()) {
                    state.contatos
                } else {
                    state.buscaDeContatos
                }
                LazyColumn(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                    items(lista) { contato ->
                        ContatoItem(contato) { idContato ->
                            onClickAbreDetalhes(idContato)
                        }
                    }
                }
            }
        }
    }
}

Código do UI State

data class ListaContatosUiState(
    val contatos: List<Contato> = emptyList(),
    val buscaDeContatos: List<Contato> = emptyList(),
    val logado: Boolean = true,
    val textoDeBusca: String = "",
    val alteracaoDoTextoDeBusca: (String) -> Unit = {}
){
    fun textoDeBuscaEstaEmBranco() = textoDeBusca.isBlank()
}

código da barra de busca:

@Composable
private fun IconeDeBusca(
    modifier: Modifier = Modifier,
    state: ListaContatosUiState = ListaContatosUiState(),
) {
    OutlinedTextField(
        modifier = modifier
            .fillMaxWidth(),
        shape = RoundedCornerShape(100),
        value = state.textoDeBusca,
        onValueChange = state.alteracaoDoTextoDeBusca,
        leadingIcon = {
            Icon(imageVector = Icons.Default.Search, contentDescription = "icone de busca")
        },
        label = {
            Text(text = "Contato")
        },
        placeholder = {
            Text(text = "Procurar pelo nome do Contato")
        },
        colors = TextFieldDefaults.colors(
            focusedTextColor = Color.Gray,
            focusedPlaceholderColor = Color.LightGray,
            focusedContainerColor = Color.White
        ),
    )
}
4 respostas

Não precisa mais, consegui resolver utilizando o run blocking com ajuda desse fórum:

    private fun buscaDeContatos(nome: String): List<Contato> = runBlocking {
            return@runBlocking contatoDao.buscaPorNome(nome).first()
    }

Mas se tiverem sugestões de melhoria no código, ou uma abordagem melhor do que o runBlocking, podem falar (por isso que eu ainda não coloquei como solucionado esse fórum)

solução!

Boa tarde Murilo.

A solução final que você encontrou provavelmente funcionou não por causa do runBlocking mas pela forma que o método está sendo chamado agora.

O retorno de buscaDeContatos agora é a unica instrução a ser executada.

Já no seu primeiro código, algumas tarefas são executadas em paralelo e o retorno é chamado antes mesmo da operção de busca ser concluída.

private fun buscaDeContatos(nome: String): List<Contato> {
    var contatosBuscados: List<Contato> = emptyList()
    viewModelScope.launch {
    // eu acho que eu precisaria garantir que isso seja buscado antes que o view model pegue o resultado
        contatosBuscados = contatoDao.buscaPorNome(nome).first()
    }
    return contatosBuscados
}

Ao mover return contatosBuscados para dentro do bloco viewModelScope.launch {...} você obterá o resultado esperado, pois o retorno só será feito após a busca ser concluída:

private fun buscaDeContatos(nome: String): List<Contato> {
    var contatosBuscados: List<Contato> = emptyList()
    viewModelScope.launch {
    // eu acho que eu precisaria garantir que isso seja buscado antes que o view model pegue o resultado
        contatosBuscados = contatoDao.buscaPorNome(nome).first()
            return contatosBuscados
    }
}

Espero que tenha ajudado e bons estudos!

Na verdade, quando eu copiei o seu código ele apresentou um problema, o retorno não acontece pois o return está dentro do view Model Scope. E aparece essa mensagem:

'return' is not allowed here
private fun buscaDeContatos(nome: String): List<Contato> {
    var contatosBuscados: List<Contato> = emptyList()
    viewModelScope.launch {
    contatosBuscados = contatoDao.buscaPorNome(nome).first()
    //ele não permite return dentro do viewModelScope
            return contatosBuscados
    }
}

A minha solução com o runBlocking era garantir que o bloco rode a todo antes de retornar a resposta. E também para conseguir retornar mesmo estando dentro de uma coroutines Scope

    private fun buscaDeContatos(nome: String): List<Contato> = runBlocking {
            return@runBlocking contatoDao.buscaPorNome(nome).first()
    }

Murilo realmente, o ideal para essa abordagem funcionar com o seu código seria através do uso de high order functions, que são muito comuns na programação Android com Kotlin:

    private fun buscaDeContatos(nome: String, onContatosBuscados: (List<Contato>) -> Unit) {
        var contatosBuscados: List<Contato> = emptyList()
        viewModelScope.launch {
            contatosBuscados = contatoDao.buscaPorNome(nome).first()
            onContatosBuscados(contatosBuscados)
        }
    }

E na chamada do método fazer algo como isto:

alteracaoDoTextoDeBusca = {
    _uiState.value = uiState.value.copy(
        textoDeBusca = it,
        // método de busca seja atualizado junto com o texto da barra de pesquisa
        // buscaDeContatos = buscaDeContatos(it)
        buscaDeContatos(it) { contatosBuscados ->
            // Usar a lista "contatosBuscados" para algo
        }
    )
}

Essa abordagem é útil quando precisamos lidar com uma operação que não sabemos quando irá terminar, como por exemplo a busca em um banco muito grande ou uma requisição a um serviço externo.

Lembrando que você pode conferir a solução oficial através desta branch, lá implementarmos uma solução mais simples para este problema.