Loading app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +8 −18 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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) } } } } Loading Loading @@ -333,7 +333,6 @@ class SearchFragment : } private fun initiateSearch() { showLoadingUi() searchViewModel.loadData(searchText) } Loading Loading @@ -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) } Loading Loading @@ -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 } } app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +26 −12 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) 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, Loading @@ -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 { Loading Loading
app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +8 −18 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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) } } } } Loading Loading @@ -333,7 +333,6 @@ class SearchFragment : } private fun initiateSearch() { showLoadingUi() searchViewModel.loadData(searchText) } Loading Loading @@ -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) } Loading Loading @@ -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 } }
app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +26 −12 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) 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, Loading @@ -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 { Loading