Refactored the KiwixSearchView to use externally provided placeholder for searchView.

* Created the `PageFragmentScreenState` to encapsulates the all UI-related state to reduce the complexity in fragment.
This commit is contained in:
MohitMaliFtechiz 2025-04-11 18:13:55 +05:30
parent acbf1bfec3
commit 1d3944dab5
4 changed files with 165 additions and 92 deletions

View File

@ -28,31 +28,26 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.referentialEqualityPolicy import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.core.view.MenuHost import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.databinding.FragmentPageBinding import org.kiwix.kiwixmobile.core.databinding.FragmentPageBinding
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.closeKeyboard import org.kiwix.kiwixmobile.core.extensions.closeKeyboard
import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
import org.kiwix.kiwixmobile.core.extensions.setUpSearchView import org.kiwix.kiwixmobile.core.extensions.setUpSearchView
import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener
@ -98,19 +93,28 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
policy = referentialEqualityPolicy() policy = referentialEqualityPolicy()
) )
/** private val pageScreenState = mutableStateOf(
* Controls the visibility of the "Switch", and its controls. // Initial values are empty because this is an abstract class.
* // Before the view is created, the abstract variables have no values.
* A [Triple] containing: // We update this state in `onViewCreated`, once the view is created and the
* - [String]: The text displayed with switch. // abstract variables are initialized.
* - [Boolean]: Whether the switch is checked or not. PageFragmentScreenState(
* - [Boolean]: Whether the switch is enabled or disabled. pageState = pageState.value,
*/ isSearchActive = false,
private val pageSwitchItem = mutableStateOf(Triple("", true, true)) searchQueryHint = "",
searchText = "",
searchValueChangedListener = {},
screenTitle = ZERO,
noItemsString = "",
switchString = "",
switchIsChecked = true,
switchIsEnabled = true,
onSwitchCheckedChanged = {},
deleteIconTitle = ZERO,
clearSearchButtonClickListener = {}
)
)
private var fragmentPageBinding: FragmentPageBinding? = null private var fragmentPageBinding: FragmentPageBinding? = null
override val fragmentToolbar: Toolbar? by lazy {
fragmentPageBinding?.root?.findViewById(R.id.toolbar)
}
private val actionModeCallback: ActionMode.Callback = private val actionModeCallback: ActionMode.Callback =
object : ActionMode.Callback { object : ActionMode.Callback {
@ -177,15 +181,28 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
pageScreenState.value = pageScreenState.value.copy(
isSearchActive = pageScreenState.value.isSearchActive,
searchQueryHint = searchQueryHint,
searchText = "",
searchValueChangedListener = { onTextChanged(it) },
clearSearchButtonClickListener = { onTextChanged("") },
screenTitle = screenTitle,
noItemsString = noItemsString,
switchString = switchString,
switchIsChecked = pageScreenState.value.switchIsChecked,
onSwitchCheckedChanged = { onSwitchCheckedChanged(it) },
deleteIconTitle = deleteIconTitle
)
// setupMenu() // setupMenu()
val activity = requireActivity() as CoreMainActivity val activity = requireActivity() as CoreMainActivity
fragmentPageBinding?.recyclerView?.apply { // fragmentPageBinding?.recyclerView?.apply {
layoutManager = // layoutManager =
LinearLayoutManager(activity, RecyclerView.VERTICAL, false) // LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
adapter = pageAdapter // adapter = pageAdapter
fragmentTitle?.let(::setToolTipWithContentDescription) // fragmentTitle?.let(::setToolTipWithContentDescription)
} // }
fragmentPageBinding?.noPage?.text = noItemsString // fragmentPageBinding?.noPage?.text = noItemsString
// fragmentPageBinding?.pageSwitch?.apply { // fragmentPageBinding?.pageSwitch?.apply {
// text = switchString // text = switchString
@ -216,24 +233,20 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
): View? { ): View? {
return ComposeView(requireContext()).apply { return ComposeView(requireContext()).apply {
setContent { setContent {
val isSearchActive = remember { mutableStateOf(false) }
PageScreen( PageScreen(
pageState = pageState.value, state = pageScreenState.value,
pageSwitchItem = pageSwitchItem.value,
screenTitle = screenTitle,
noItemsString = noItemsString,
searchQueryHint = searchQueryHint,
onSwitchChanged = { onSwitchCheckedChanged(it) },
itemClickListener = this@PageFragment, itemClickListener = this@PageFragment,
navigationIcon = { navigationIcon = {
NavigationIcon( NavigationIcon(
iconItem = navigationIconItem(isSearchActive.value), iconItem = navigationIconItem(pageScreenState.value.isSearchActive),
onClick = navigationIconClick(isSearchActive) onClick = navigationIconClick()
) )
}, },
actionMenuItems = actionMenuList( actionMenuItems = actionMenuList(
isSearchActive = isSearchActive.value, isSearchActive = pageScreenState.value.isSearchActive,
onSearchClick = { isSearchActive.value = true }, onSearchClick = {
pageScreenState.value = pageScreenState.value.copy(isSearchActive = true)
},
onDeleteClick = { pageViewModel.actions.offer(Action.UserClickedDeleteButton) } onDeleteClick = { pageViewModel.actions.offer(Action.UserClickedDeleteButton) }
) )
) )
@ -241,8 +254,13 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
} }
} }
private fun onTextChanged(searchText: String) {
pageScreenState.value = pageScreenState.value.copy(searchText = searchText)
pageViewModel.actions.offer(Action.Filter(searchText))
}
private fun onSwitchCheckedChanged(isChecked: Boolean): () -> Unit = { private fun onSwitchCheckedChanged(isChecked: Boolean): () -> Unit = {
pageSwitchItem.value = pageSwitchItem.value.copy(second = isChecked) pageScreenState.value = pageScreenState.value.copy(switchIsChecked = isChecked)
pageViewModel.actions.offer(Action.UserClickedShowAllToggle(isChecked)) pageViewModel.actions.offer(Action.UserClickedShowAllToggle(isChecked))
} }
@ -252,9 +270,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack) IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack)
} }
private fun navigationIconClick(isSearchActive: MutableState<Boolean>): () -> Unit = { private fun navigationIconClick(): () -> Unit = {
if (isSearchActive.value) { if (pageScreenState.value.isSearchActive) {
isSearchActive.value = false pageScreenState.value = pageScreenState.value.copy(isSearchActive = false)
pageViewModel.actions.offer(Action.Exit) pageViewModel.actions.offer(Action.Exit)
} else { } else {
requireActivity().onBackPressedDispatcher.onBackPressed() requireActivity().onBackPressedDispatcher.onBackPressed()
@ -293,8 +311,10 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv
} }
private fun render(state: PageState<*>) { private fun render(state: PageState<*>) {
pageState.value = state pageScreenState.value = pageScreenState.value.copy(
pageSwitchItem.value = Triple(switchString, switchIsChecked, !state.isInSelectionState) switchIsEnabled = !state.isInSelectionState,
pageState = state,
)
if (state.isInSelectionState) { if (state.isInSelectionState) {
if (actionMode == null) { if (actionMode == null) {
actionMode = actionMode =

View File

@ -0,0 +1,45 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.page
import androidx.annotation.StringRes
import org.kiwix.kiwixmobile.core.page.viewmodel.PageState
/**
* Represents the UI state for the PageFragment Screen.
* A Base screen for Bookmarks, History, and Notes screens.
*
* This data class encapsulates all UI-related states in a single object,
* reducing complexity in the Fragment.
*/
data class PageFragmentScreenState(
val pageState: PageState<*>,
val isSearchActive: Boolean,
val searchQueryHint: String,
val searchText: String,
val searchValueChangedListener: (String) -> Unit,
val clearSearchButtonClickListener: () -> Unit,
@StringRes val screenTitle: Int,
val noItemsString: String,
val switchString: String,
val switchIsChecked: Boolean,
val switchIsEnabled: Boolean = true,
val onSwitchCheckedChanged: (Boolean) -> Unit,
@StringRes val deleteIconTitle: Int
)

View File

@ -19,6 +19,7 @@
package org.kiwix.kiwixmobile.core.page package org.kiwix.kiwixmobile.core.page
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -39,89 +40,95 @@ import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener
import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.DateItem import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.DateItem
import org.kiwix.kiwixmobile.core.page.viewmodel.PageState
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
@Suppress( @Suppress("ComposableLambdaParameterNaming")
"LongParameterList",
"IgnoredReturnValue",
"UnusedParameter",
"ComposableLambdaParameterNaming"
)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PageScreen( fun PageScreen(
pageState: PageState<out Page>, state: PageFragmentScreenState,
pageSwitchItem: Triple<String, Boolean, Boolean>,
screenTitle: Int,
noItemsString: String,
searchQueryHint: String,
onSwitchChanged: (Boolean) -> Unit,
itemClickListener: OnItemClickListener, itemClickListener: OnItemClickListener,
actionMenuItems: List<ActionMenuItem>, actionMenuItems: List<ActionMenuItem>,
navigationIcon: @Composable () -> Unit, navigationIcon: @Composable () -> Unit
) { ) {
val context = LocalActivity.current as CoreMainActivity
KiwixTheme { KiwixTheme {
Scaffold( Scaffold(
topBar = { topBar = {
Column { Column {
KiwixAppBar( KiwixAppBar(
titleId = screenTitle, titleId = state.screenTitle,
navigationIcon = navigationIcon, navigationIcon = navigationIcon,
actionMenuItems = actionMenuItems actionMenuItems = actionMenuItems,
searchBar = searchBarIfActive(state)
) )
// hide switches for custom apps, see more info here https://github.com/kiwix/kiwix-android/issues/3523 PageSwitchRow(state)
if (!context.isCustomApp()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SIXTEEN_DP, vertical = EIGHT_DP),
verticalAlignment = Alignment.CenterVertically
) {
Text(pageSwitchItem.first, modifier = Modifier.weight(1f))
Switch(
checked = pageSwitchItem.second,
onCheckedChange = onSwitchChanged,
enabled = pageSwitchItem.third
)
}
}
} }
} }
) { padding -> ) { padding ->
val items = pageState.pageItems val items = state.pageState.pageItems
Box(modifier = Modifier.padding(padding)) { Box(modifier = Modifier.padding(padding)) {
if (items.isEmpty()) { if (items.isEmpty()) {
Text( Text(
text = noItemsString, text = state.noItemsString,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
} else { } else {
LazyColumn { LazyColumn {
items(pageState.visiblePageItems) { item -> items(state.pageState.visiblePageItems) { item ->
when (item) { when (item) {
is Page -> { is Page -> PageListItem(page = item, itemClickListener = itemClickListener)
PageListItem( is DateItem -> DateItemText(item)
page = item,
itemClickListener = itemClickListener
)
} }
}
}
}
}
}
}
}
is DateItem -> { @Composable
DateItemText(item) private fun searchBarIfActive(
} state: PageFragmentScreenState
} ): (@Composable () -> Unit)? = {
} if (state.isSearchActive) {
} KiwixSearchView(
} placeholder = state.searchQueryHint,
value = state.searchText,
testTag = "",
onValueChange = { state.searchValueChangedListener(it) },
onClearClick = { state.clearSearchButtonClickListener.invoke() }
)
} else {
null
} }
}
@Composable
fun PageSwitchRow(
state: PageFragmentScreenState
) {
val context = LocalActivity.current as CoreMainActivity
// hide switches for custom apps, see more info here https://github.com/kiwix/kiwix-android/issues/3523
if (!context.isCustomApp()) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(Black),
verticalAlignment = Alignment.CenterVertically
) {
Text(state.switchString)
Switch(
checked = state.switchIsChecked,
onCheckedChange = { state.onSwitchCheckedChanged(it) },
enabled = state.switchIsEnabled
)
} }
} }
} }

View File

@ -39,8 +39,9 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens
@Composable @Composable
fun KiwixSearchView( fun KiwixSearchView(
modifier: Modifier, modifier: Modifier = Modifier,
value: String, value: String,
placeholder: String = stringResource(R.string.search_label),
testTag: String = "", testTag: String = "",
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
onClearClick: () -> Unit onClearClick: () -> Unit
@ -65,7 +66,7 @@ fun KiwixSearchView(
value = value, value = value,
placeholder = { placeholder = {
Text( Text(
text = stringResource(R.string.search_label), text = placeholder,
color = Color.LightGray, color = Color.LightGray,
fontSize = ComposeDimens.EIGHTEEN_SP fontSize = ComposeDimens.EIGHTEEN_SP
) )