Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 56c67486 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: migrate search suggestions to use state flow with debounce

parent 85fa5156
Loading
Loading
Loading
Loading
+8 −18
Original line number Diff line number Diff line
@@ -60,9 +60,7 @@ import foundation.e.apps.ui.PrivacyInfoViewModel
import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment
import foundation.e.apps.ui.applicationlist.ApplicationListRVAdapter
import foundation.e.apps.utils.isNetworkAvailable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.Locale
@@ -131,8 +129,6 @@ class SearchFragment :

        setupSearchFilters()

        initiateSearch()

        binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
@@ -266,8 +262,12 @@ class SearchFragment :
            CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER
        )

        searchViewModel.searchSuggestions.observe(viewLifecycleOwner) {
            it?.let { populateSuggestionsAdapter(it) }
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                searchViewModel.searchSuggestions.collectLatest {
                    populateSuggestionsAdapter(it)
                }
            }
        }
    }

@@ -333,7 +333,6 @@ class SearchFragment :
    }

    private fun initiateSearch() {
        showLoadingUi()
        searchViewModel.loadData(searchText)
    }

@@ -421,24 +420,16 @@ class SearchFragment :
    }

    override fun onQueryTextChange(newText: String?): Boolean {
        newText?.takeIf { it.isNotEmpty() }?.let(::doDebouncedSearch)
        searchViewModel.onQueryChanged(newText.orEmpty())
        return true
    }

    private fun doDebouncedSearch(text: String) {
        searchJob?.cancel()
        searchJob = lifecycleScope.launch(Dispatchers.Main.immediate) {
            delay(SEARCH_DEBOUNCE_DELAY_MILLIS)
            searchViewModel.getSearchSuggestions(text)
        }
    }

    override fun onSuggestionSelect(position: Int): Boolean {
        return true
    }

    override fun onSuggestionClick(position: Int): Boolean {
        searchViewModel.searchSuggestions.value?.let {
        searchViewModel.searchSuggestions.value.let {
            if (it.isNotEmpty()) {
                searchView?.setQuery(it[position].suggestion, true)
            }
@@ -504,7 +495,6 @@ class SearchFragment :
    }

    companion object {
        private const val SEARCH_DEBOUNCE_DELAY_MILLIS = 500L
        private const val SCROLL_TO_TOP_DELAY_MILLIS = 100L
    }
}
+26 −12
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@
package foundation.e.apps.ui.search

import androidx.annotation.GuardedBy
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -33,11 +32,18 @@ import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository
import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -53,7 +59,9 @@ class SearchViewModel @Inject constructor(
    private val appPrivacyInfoRepository: IAppPrivacyInfoRepository
) : ViewModel() {

    val searchSuggestions: MutableLiveData<List<SearchSuggestion>> = MutableLiveData()
    companion object {
        private const val SEARCH_DEBOUNCE_DELAY_MILLIS = 500L
    }

    private val _searchUiState: MutableStateFlow<SearchResultsUiState> =
        MutableStateFlow(SearchResultsUiState.Loading)
@@ -69,6 +77,22 @@ class SearchViewModel @Inject constructor(
    private var flagOpenSource: Boolean = false
    private var flagPWA: Boolean = false

    val query = MutableStateFlow("")

    fun onQueryChanged(value: String) {
        query.value = value
    }

    @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
    val searchSuggestions: StateFlow<List<SearchSuggestion>> = query
        .debounce(SEARCH_DEBOUNCE_DELAY_MILLIS)
        .mapLatest {
            if (it.isBlank()) emptyList()
            else searchRepository.getSearchSuggestions(it)
        }
        .distinctUntilChanged()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())

    fun setFilterFlags(
        flagNoTrackers: Boolean = false,
        flagOpenSource: Boolean = false,
@@ -83,17 +107,7 @@ class SearchViewModel @Inject constructor(
        }
    }

    fun getSearchSuggestions(query: String) {
        viewModelScope.launch(Dispatchers.IO) {
            searchSuggestions.postValue(
                searchRepository.getSearchSuggestions(query)
            )
        }
    }

    fun loadData(query: String) {
        if (query.isBlank()) return

        _searchUiState.value = SearchResultsUiState.Loading

        viewModelScope.launch {