From 345603a2a301d94a47d4f49bf432d7c8e3d6c0f1 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Wed, 24 Sep 2025 11:53:11 +0600 Subject: [PATCH] refactor: introduce SearchViewHandler to delegate UI logic from SearchFragment --- .../e/apps/ui/search/SearchFragment.kt | 127 +++++++----------- .../e/apps/ui/search/SearchViewHandler.kt | 100 ++++++++++++++ .../e/apps/ui/search/SearchViewModel.kt | 2 +- 3 files changed, 147 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/ui/search/SearchViewHandler.kt 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 d1a4ad857..962fdb715 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 @@ -21,18 +21,13 @@ package foundation.e.apps.ui.search import android.app.Activity import android.content.Context -import android.database.MatrixCursor import android.os.Bundle -import android.provider.BaseColumns import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.CompoundButton.OnCheckedChangeListener import android.widget.EditText -import android.widget.ImageView import android.widget.LinearLayout import androidx.appcompat.widget.SearchView -import androidx.cursoradapter.widget.CursorAdapter -import androidx.cursoradapter.widget.SimpleCursorAdapter import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels @@ -48,7 +43,6 @@ import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.application.search.SearchSuggestion import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.databinding.FragmentSearchBinding @@ -66,13 +60,9 @@ import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject -@Suppress("TooManyFunctions", "MaxLineLength") // TODO: Remove after refactoring is complete @AndroidEntryPoint -class SearchFragment : - Fragment(R.layout.fragment_search), - SearchView.OnQueryTextListener, - SearchView.OnSuggestionListener, - ApplicationInstaller { +class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, + SearchViewHandler.SearchViewListener { @Inject lateinit var pwaManager: PwaManager @@ -86,10 +76,11 @@ class SearchFragment : val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() - private val SUGGESTION_KEY = "suggestion" private var lastSearch = "" private var searchView: SearchView? = null + private var searchViewHandler: SearchViewHandler? = null + private var shimmerLayout: ShimmerFrameLayout? = null private var recyclerView: RecyclerView? = null private var searchHintLayout: LinearLayout? = null @@ -111,8 +102,8 @@ class SearchFragment : filterChipOpenSource = binding.filterChipOpenSource filterChipPWA = binding.filterChipPWA - setupSearchView() - setupSearchViewSuggestions() + setupSearchViewHandler() + setHasOptionsMenu(true) // Setup Search Results val listAdapter = setupSearchResult(view) @@ -261,19 +252,26 @@ class SearchFragment : return listAdapter } - private fun setupSearchViewSuggestions() { - val from = arrayOf(SUGGESTION_KEY) - val to = intArrayOf(android.R.id.text1) - searchView?.suggestionsAdapter = SimpleCursorAdapter( - context, - R.layout.custom_simple_list_item, null, from, to, - CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER - ) + private fun setupSearchViewHandler() { + searchView?.let { + searchViewHandler = SearchViewHandler( + searchView = it, + context = requireContext(), + listener = this, + ) + searchViewHandler?.setup() + } + + if (searchViewModel.searchText.isNotBlank()) { + searchView?.setQuery( + searchViewModel.searchText, false + ) // 'false' prevents re-submitting the query + } viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - searchViewModel.searchSuggestions.collectLatest { - populateSuggestionsAdapter(it) + searchViewModel.searchSuggestions.collectLatest { suggestions -> + searchViewHandler?.populateSuggestionsAdapter(suggestions) } } } @@ -297,13 +295,6 @@ class SearchFragment : filterChipPWA.setOnCheckedChangeListener(listener) } - private fun setupSearchView() { - setHasOptionsMenu(true) - searchView?.setOnSuggestionListener(this) - searchView?.setOnQueryTextListener(this) - searchView?.let { configureCloseButton(it) } - } - private fun showPaidAppMessage(application: Application) { ApplicationDialogFragment( title = getString(R.string.dialog_title_paid_app, application.name), @@ -388,7 +379,9 @@ class SearchFragment : } } - if (searchViewModel.searchText.isEmpty() && (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty()) { + if (searchViewModel.searchText.isEmpty() && + (recyclerView?.adapter as ApplicationListRVAdapter).currentList.isEmpty() + ) { searchView?.requestFocus() showKeyboard() } @@ -414,41 +407,6 @@ class SearchFragment : super.onPause() } - override fun onQueryTextSubmit(query: String?): Boolean { - query?.let { text -> - if (text.isNotEmpty()) { - hideKeyboard(activity as Activity) - } - - view?.requestFocus() - searchHintLayout?.visibility = View.GONE - /* - * Set the search text and call for network result. - */ - searchViewModel.searchText = text - initiateSearch() - } - return false - } - - override fun onQueryTextChange(newText: String?): Boolean { - searchViewModel.onQueryChanged(newText.orEmpty()) - return true - } - - override fun onSuggestionSelect(position: Int): Boolean { - return true - } - - override fun onSuggestionClick(position: Int): Boolean { - searchViewModel.searchSuggestions.value.let { - if (it.isNotEmpty()) { - searchView?.setQuery(it[position].suggestion, true) - } - } - return true - } - override fun onDestroyView() { super.onDestroyView() _binding = null @@ -458,13 +416,7 @@ class SearchFragment : recyclerView = null searchHintLayout = null noAppsFoundLayout = null - } - - private fun configureCloseButton(searchView: SearchView) { - val searchClose = searchView.javaClass.getDeclaredField("mCloseButton") - searchClose.isAccessible = true - val closeImage = searchClose.get(searchView) as ImageView - closeImage.setImageResource(R.drawable.ic_close) + searchViewHandler = null } private fun hideKeyboard(activity: Activity) { @@ -488,14 +440,27 @@ class SearchFragment : } } - private fun populateSuggestionsAdapter(suggestions: List?) { - val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SUGGESTION_KEY)) - suggestions?.let { - for (i in it.indices) { - cursor.addRow(arrayOf(i, it[i].suggestion)) + override fun onQuerySubmitted(query: String) { + query.let { text -> + if (text.isNotEmpty()) { + hideKeyboard(activity as Activity) } + + view?.requestFocus() + searchHintLayout?.visibility = View.GONE + + searchViewModel.searchText = text + initiateSearch() } - searchView?.suggestionsAdapter?.changeCursor(cursor) + } + + override fun onQueryChanged(newText: String) { + searchViewModel.onSearchQueryChanged(newText) + } + + override fun onSuggestionClicked(position: Int) { + val suggestion = searchViewModel.searchSuggestions.value.getOrNull(position)?.suggestion + suggestion?.let { searchView?.setQuery(it, true) } } override fun installApplication(app: Application) { diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewHandler.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewHandler.kt new file mode 100644 index 000000000..edb146cec --- /dev/null +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewHandler.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2025 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.ui.search + +import android.content.Context +import android.database.MatrixCursor +import android.provider.BaseColumns +import android.widget.ImageView +import androidx.appcompat.widget.SearchView +import androidx.cursoradapter.widget.CursorAdapter +import androidx.cursoradapter.widget.SimpleCursorAdapter +import foundation.e.apps.R +import foundation.e.apps.data.application.search.SearchSuggestion + +class SearchViewHandler( + private val searchView: SearchView, + private val context: Context, + private val listener: SearchViewListener, +) : SearchView.OnQueryTextListener, SearchView.OnSuggestionListener { + + interface SearchViewListener { + fun onQuerySubmitted(query: String) + fun onQueryChanged(newText: String) + fun onSuggestionClicked(position: Int) + } + + fun setup() { + searchView.setOnQueryTextListener(this) + searchView.setOnSuggestionListener(this) + setupSuggestions() + configureCloseButton() + } + + private fun setupSuggestions() { + val from = arrayOf(SUGGESTION_KEY) + val to = intArrayOf(android.R.id.text1) + searchView.suggestionsAdapter = SimpleCursorAdapter( + context, + R.layout.custom_simple_list_item, null, from, to, + CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER + ) + } + + internal fun populateSuggestionsAdapter(suggestions: List?) { + val cursor = MatrixCursor(arrayOf(BaseColumns._ID, SUGGESTION_KEY)) + suggestions?.let { + for (i in it.indices) { + cursor.addRow(arrayOf(i, it[i].suggestion)) + } + } + searchView.suggestionsAdapter?.changeCursor(cursor) + } + + private fun configureCloseButton() { + val searchClose = searchView.javaClass.getDeclaredField("mCloseButton") + searchClose.isAccessible = true + val closeImage = searchClose.get(searchView) as ImageView + closeImage.setImageResource(R.drawable.ic_close) + } + + override fun onQueryTextSubmit(query: String?): Boolean { + query?.let { text -> + listener.onQuerySubmitted(text) + } + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + listener.onQueryChanged(newText.orEmpty()) + return true + } + + override fun onSuggestionSelect(position: Int): Boolean { + return true + } + + override fun onSuggestionClick(position: Int): Boolean { + listener.onSuggestionClicked(position) + return true + } + + companion object { + private const val SUGGESTION_KEY = "suggestion" + } +} 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 4497bc3f9..6e89038e3 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 @@ -86,7 +86,7 @@ class SearchViewModel @Inject constructor( val query = MutableStateFlow("") - fun onQueryChanged(value: String) { + fun onSearchQueryChanged(value: String) { query.value = value } -- GitLab