From 793bdce3e5af46c00e6c3368727835743ecdec66 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 18 Sep 2025 18:28:57 +0600 Subject: [PATCH] refactor: make change in app sources reflect on search results --- .../e/apps/ui/search/SearchFragment.kt | 44 ++++++++++------ .../e/apps/ui/search/SearchResultsUiState.kt | 1 + .../e/apps/ui/search/SearchViewModel.kt | 51 +++++++++++++------ app/src/main/res/layout/fragment_search.xml | 14 ++--- 4 files changed, 74 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 3ff4084a4..d1a4ad857 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -66,7 +66,7 @@ import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject -@Suppress("TooManyFunctions") // TODO: Remove after refactoring is complete +@Suppress("TooManyFunctions", "MaxLineLength") // TODO: Remove after refactoring is complete @AndroidEntryPoint class SearchFragment : Fragment(R.layout.fragment_search), @@ -98,12 +98,6 @@ class SearchFragment : lateinit var filterChipOpenSource: Chip lateinit var filterChipPWA: Chip - /* - * Store the string from onQueryTextSubmit() and access it from loadData() - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 - */ - private var searchText = "" - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentSearchBinding.bind(view) @@ -134,7 +128,7 @@ class SearchFragment : if (!requireContext().isNetworkAvailable()) { return } - searchViewModel.loadMore(searchText) + searchViewModel.loadMore() } } }) @@ -145,26 +139,42 @@ class SearchFragment : viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { searchViewModel.searchUiState.collectLatest { state -> when (state) { + is SearchResultsUiState.Initial -> { + searchHintLayout?.visibility = View.VISIBLE + noAppsFoundLayout?.visibility = View.GONE + } + is SearchResultsUiState.Loading -> { noAppsFoundLayout?.visibility = View.GONE + searchHintLayout?.visibility = View.GONE showLoadingUi() } is SearchResultsUiState.Success -> { + noAppsFoundLayout?.visibility = View.GONE + searchHintLayout?.visibility = View.GONE + listAdapter?.let { observeDownloadList(it) } updateSearchResult(listAdapter, state.apps) observeScrollOfSearchResult(listAdapter) } is SearchResultsUiState.Empty -> { + searchHintLayout?.visibility = View.GONE + noAppsFoundLayout?.visibility = View.GONE + stopLoadingUi() + noAppsFoundLayout?.visibility = View.VISIBLE + listAdapter?.setData(emptyList()) } is SearchResultsUiState.Error -> { - stopLoadingUi() + searchHintLayout?.visibility = View.GONE noAppsFoundLayout?.visibility = View.VISIBLE + + stopLoadingUi() } } } @@ -186,9 +196,9 @@ class SearchFragment : * Compare lastSearch with searchText to avoid falsely updating to * current query text even before submitting the new search. */ - if (lastSearch != searchText && positionStart == 0) { + if (lastSearch != searchViewModel.searchText && positionStart == 0) { scrollToTop() - lastSearch = searchText + lastSearch = searchViewModel.searchText } } } @@ -331,7 +341,7 @@ class SearchFragment : } private fun initiateSearch() { - searchViewModel.loadData(searchText) + searchViewModel.loadData() } private fun showLoadingUi() { @@ -378,10 +388,14 @@ class SearchFragment : } } - if (searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) { + if (searchViewModel.searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) { searchView?.requestFocus() showKeyboard() } + + if (searchViewModel.searchText.isNotBlank() && searchViewModel.haveSourcesChanged()) { + initiateSearch() + } } private fun addDownloadProgressObservers() { @@ -392,7 +406,7 @@ class SearchFragment : } private fun shouldRefreshData() = - searchText.isNotEmpty() && recyclerView?.adapter != null + searchViewModel.searchText.isNotEmpty() && recyclerView?.adapter != null override fun onPause() { binding.shimmerLayout.stopShimmer() @@ -411,7 +425,7 @@ class SearchFragment : /* * Set the search text and call for network result. */ - searchText = text + searchViewModel.searchText = text initiateSearch() } return false diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchResultsUiState.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchResultsUiState.kt index 000d1a37d..7639e0b74 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchResultsUiState.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchResultsUiState.kt @@ -27,6 +27,7 @@ import foundation.e.apps.data.application.data.Application * - Error: failure with optional message/throwable */ sealed interface SearchResultsUiState { + data object Initial : SearchResultsUiState data object Loading : SearchResultsUiState data class Success(val apps: List) : SearchResultsUiState data object Empty : SearchResultsUiState diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index fdbb4e9ad..4497bc3f9 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -23,6 +23,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.StoreRepository +import foundation.e.apps.data.Stores import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.search.SearchRepository @@ -48,7 +50,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -56,7 +57,8 @@ class SearchViewModel @Inject constructor( private val applicationRepository: ApplicationRepository, private val searchRepository: SearchRepository, private val privacyScoreRepository: PrivacyScoreRepository, - private val appPrivacyInfoRepository: IAppPrivacyInfoRepository + private val appPrivacyInfoRepository: IAppPrivacyInfoRepository, + private val stores: Stores, ) : ViewModel() { companion object { @@ -64,10 +66,13 @@ class SearchViewModel @Inject constructor( } private val _searchUiState: MutableStateFlow = - MutableStateFlow(SearchResultsUiState.Loading) + MutableStateFlow(SearchResultsUiState.Initial) val searchUiState: StateFlow = _searchUiState.asStateFlow() private var isLoading: Boolean = false + private var isCleanApkDataLoading = false + + private var previousStores = mapOf() @GuardedBy("mutex") private val accumulatedList = mutableListOf() @@ -77,6 +82,8 @@ class SearchViewModel @Inject constructor( private var flagOpenSource: Boolean = false private var flagPWA: Boolean = false + var searchText = "" + val query = MutableStateFlow("") fun onQueryChanged(value: String) { @@ -107,33 +114,47 @@ class SearchViewModel @Inject constructor( } } - fun loadData(query: String) { - _searchUiState.value = SearchResultsUiState.Loading - + fun loadData() { viewModelScope.launch { + _searchUiState.value = SearchResultsUiState.Loading + mutex.withLock { accumulatedList.clear() } - fetchCleanApkData(query) - fetchPlayStoreData(query) + fetchCleanApkData(searchText) + fetchPlayStoreData(searchText) } } + fun haveSourcesChanged(): Boolean { + val newStores = stores.getStores() + if (newStores == previousStores) { + return false + } + + previousStores = newStores.toMap() + return true + } + private fun fetchCleanApkData(query: String) { viewModelScope.launch(Dispatchers.IO) { + isCleanApkDataLoading = true + val searchResults = searchRepository.getOpenSourceSearchResults(query) + isCleanApkDataLoading = false + emitFilteredResults(searchResults) + } } - fun loadMore(query: String) { + fun loadMore() { viewModelScope.launch(Dispatchers.Main) { if (isLoading) { - Timber.d("Search result is loading....") return@launch } - fetchPlayStoreData(query) + fetchPlayStoreData(searchText) } } @@ -235,10 +256,10 @@ class SearchViewModel @Inject constructor( val filteredList = getFilteredList() - _searchUiState.value = if (filteredList.isEmpty()) { - SearchResultsUiState.Empty - } else { - SearchResultsUiState.Success(filteredList.toList()) + _searchUiState.value = when { + isCleanApkDataLoading -> SearchResultsUiState.Loading + filteredList.isEmpty() -> SearchResultsUiState.Empty + else -> SearchResultsUiState.Success(filteredList.toList()) } } } diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 8d6a4022b..b6a29ac8d 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -1,6 +1,5 @@ + android:visibility="gone" + tools:visibility="visible"> - \ No newline at end of file + -- GitLab