feat: language fragment to jetpack compose

- further decoupled the code, shortened the functions.
- fixed all detekt long method errors
- removed a lot of redundant code
This commit is contained in:
jaskaran 2025-03-20 01:49:53 +05:30 committed by MohitMaliFtechiz
parent e6271e9977
commit 1cffeb5eac
4 changed files with 195 additions and 171 deletions

View File

@ -18,45 +18,121 @@
package org.kiwix.kiwixmobile.language
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModelProvider
import io.reactivex.disposables.CompositeDisposable
import org.kiwix.kiwixmobile.cachedComponent
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.extensions.viewModel
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.language.composables.AppBarNavigationIcon
import org.kiwix.kiwixmobile.language.composables.AppBarTextField
import org.kiwix.kiwixmobile.language.viewmodel.Action
import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
import javax.inject.Inject
const val SEARCH_ICON_TESTING_TAG = "search"
const val SAVE_ICON_TESTING_TAG = "saveLanguages"
const val SEARCH_FIELD_TESTING_TAG = "searchField"
class LanguageFragment : BaseFragment() {
private val languageViewModel by lazy { viewModel<LanguageViewModel>(viewModelFactory) }
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
val activity = requireActivity() as CoreMainActivity
private lateinit var composeView: ComposeView
private val compositeDisposable = CompositeDisposable()
override fun inject(baseActivity: BaseActivity) {
baseActivity.cachedComponent.inject(this)
}
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as CoreMainActivity
composeView.setContent {
LanguageScreen(
viewModelState = languageViewModel.state,
onNavigationClick = activity.onBackPressedDispatcher::onBackPressed,
selectLanguageItem = { languageViewModel.actions.offer(Action.Select(it)) },
filterText = { languageViewModel.actions.offer(Action.Filter(it)) },
saveLanguages = { languageViewModel.actions.offer(Action.SaveAll) },
)
var searchText by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) }
fun resetSearchState() {
// clears the search text and resets the filter
searchText = ""
languageViewModel.actions.offer(Action.Filter(searchText))
}
KiwixTheme {
Scaffold(topBar = {
KiwixAppBar(
titleId = R.string.select_languages,
navigationIcon = {
AppBarNavigationIcon(
isSearchActive = isSearchActive,
onClick = {
if (isSearchActive) {
isSearchActive = false
resetSearchState()
} else {
activity.onBackPressedDispatcher.onBackPressed()
}
}
)
},
actionMenuItems = appBarActionMenuList(
searchText = searchText,
isSearchActive = isSearchActive,
onSearchClick = {
isSearchActive = true
},
onClearClick = {
resetSearchState()
},
onSaveClick = {
languageViewModel.actions.offer(Action.SaveAll)
}
),
searchBar = if (isSearchActive) {
{
AppBarTextField(
value = searchText,
onValueChange = {
searchText = it
languageViewModel.actions.offer(Action.Filter(it))
}
)
}
} else {
null
}
)
}) {
LanguageScreen(
languageViewModel = languageViewModel
)
}
}
}
compositeAdd(activity)
}
fun compositeAdd(activity: CoreMainActivity) {
compositeDisposable.add(
languageViewModel.effects.subscribe(
{
@ -67,6 +143,41 @@ class LanguageFragment : BaseFragment() {
)
}
fun appBarActionMenuList(
searchText: String,
isSearchActive: Boolean,
onSearchClick: () -> Unit,
onClearClick: () -> Unit,
onSaveClick: () -> Unit
): List<ActionMenuItem> {
return listOfNotNull(
when {
!isSearchActive -> ActionMenuItem(
icon = IconItem.Drawable(R.drawable.action_search),
contentDescription = R.string.search_label,
onClick = onSearchClick,
testingTag = SEARCH_ICON_TESTING_TAG
)
searchText.isNotEmpty() -> ActionMenuItem(
icon = IconItem.Drawable(R.drawable.ic_clear_white_24dp),
contentDescription = R.string.search_label,
onClick = onClearClick,
testingTag = ""
)
else -> null // Handle the case when both conditions are false
},
// Second item: always included
ActionMenuItem(
icon = IconItem.Vector(Icons.Default.Check),
contentDescription = R.string.save_languages,
onClick = onSaveClick,
testingTag = SAVE_ICON_TESTING_TAG
)
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,

View File

@ -18,182 +18,63 @@
package org.kiwix.kiwixmobile.language
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.language.composables.AppBarTextField
import org.kiwix.kiwixmobile.language.composables.LanguageList
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.composables.LoadingIndicator
import org.kiwix.kiwixmobile.language.viewmodel.Action
import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
import org.kiwix.kiwixmobile.language.viewmodel.State
import org.kiwix.kiwixmobile.language.viewmodel.State.Content
const val SEARCH_ICON_TESTING_TAG = "search"
const val SAVE_ICON_TESTING_TAG = "saveLanguages"
const val SEARCH_FIELD_TESTING_TAG = "searchField"
@Suppress("all")
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun LanguageScreen(
viewModelState: MutableLiveData<State>,
selectLanguageItem: (LanguageItem) -> Unit,
filterText: (String) -> Unit,
onNavigationClick: () -> Unit,
saveLanguages: () -> Unit
languageViewModel: LanguageViewModel
) {
val state by viewModelState.observeAsState(State.Loading)
val state by languageViewModel.state.observeAsState(State.Loading)
val context = LocalContext.current
var searchText by remember { mutableStateOf("") }
var isSearchActive by remember { mutableStateOf(false) }
var updateListState by remember { mutableStateOf(false) }
val listState: LazyListState = rememberLazyListState()
KiwixTheme {
Scaffold(
topBar = {
KiwixAppBar(
R.string.select_languages,
{
NavigationIcon(
iconItem = if (isSearchActive) {
IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack)
} else {
IconItem.Drawable(
R.drawable.ic_close_white_24dp
)
},
onClick = {
if (isSearchActive) {
isSearchActive = false
searchText = ""
filterText(searchText)
} else {
onNavigationClick()
}
Column(modifier = Modifier.fillMaxSize()) {
// spacer to account for top app bar
Spacer(modifier = Modifier.height(56.dp))
when (state) {
State.Loading, State.Saving -> {
LoadingIndicator()
}
is Content -> {
val viewItem = (state as Content).viewItems
LaunchedEffect(viewItem) {
snapshotFlow(listState::firstVisibleItemIndex)
.collect {
if (listState.firstVisibleItemIndex == 2) {
listState.animateScrollToItem(0)
}
)
},
listOfNotNull(
// First item: conditionally include based on search state
when {
!isSearchActive -> ActionMenuItem(
icon = IconItem.Drawable(R.drawable.action_search),
contentDescription = R.string.search_label,
onClick = {
isSearchActive = true
},
iconTint = Color.White,
isEnabled = true,
testingTag = SEARCH_ICON_TESTING_TAG
)
searchText.isNotEmpty() -> ActionMenuItem(
icon = IconItem.Drawable(R.drawable.ic_clear_white_24dp),
contentDescription = R.string.search_label,
onClick = {
searchText = ""
filterText(searchText)
},
iconTint = Color.White,
isEnabled = true,
testingTag = ""
)
else -> null // Handle the case when both conditions are false
},
// Second item: always included
ActionMenuItem(
icon = IconItem.Vector(Icons.Default.Check),
contentDescription = R.string.save_languages,
onClick = {
saveLanguages()
updateListState = true
},
iconTint = Color.White,
isEnabled = true,
testingTag = SAVE_ICON_TESTING_TAG
)
),
searchBar = if (isSearchActive) {
{
AppBarTextField(
value = searchText,
onValueChange = {
searchText = it
filterText(it)
},
testTag = SEARCH_FIELD_TESTING_TAG,
hint = stringResource(R.string.search_label)
)
}
} else {
null
}
LanguageList(
context = context,
listState = listState,
viewItem = viewItem,
selectLanguageItem = { languageItem ->
languageViewModel.actions.offer(Action.Select(languageItem))
}
)
}
) {
Column(modifier = Modifier.fillMaxSize()) {
// spacer to account for top app bar
Spacer(modifier = Modifier.height(56.dp))
when (state) {
State.Loading, State.Saving -> {
LoadingIndicator()
}
is Content -> {
val viewItem = if (!updateListState) {
(state as Content).viewItems
} else {
emptyList()
}
LaunchedEffect(viewItem) {
snapshotFlow(listState::firstVisibleItemIndex)
.collect {
if (listState.firstVisibleItemIndex == 2) {
listState.animateScrollToItem(0)
}
}
}
LanguageList(
context = context,
listState = listState,
viewItem = viewItem,
selectLanguageItem = {
selectLanguageItem(it)
}
)
}
}
}
}
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.language.composables
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
import org.kiwix.kiwixmobile.core.ui.models.IconItem
@Composable
fun AppBarNavigationIcon(
isSearchActive: Boolean,
onClick: () -> Unit
) {
NavigationIcon(
iconItem = if (isSearchActive) {
IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack)
} else {
IconItem.Drawable(
R.drawable.ic_close_white_24dp
)
},
onClick = onClick
)
}

View File

@ -29,7 +29,6 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
@ -43,18 +42,18 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.language.SEARCH_FIELD_TESTING_TAG
@Suppress("all")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppBarTextField(
value: String,
hint: String,
testTag: String,
onValueChange: (String) -> Unit
) {
val interactionSource = remember(::MutableInteractionSource)
@ -68,29 +67,20 @@ fun AppBarTextField(
unfocusedContainerColor = Color.Transparent,
focusedTextColor = Color.White
)
val focusRequester = FocusRequester()
SideEffect(focusRequester::requestFocus)
var textFieldValue by remember {
mutableStateOf(TextFieldValue(value, TextRange(value.length)))
}
textFieldValue = textFieldValue.copy(text = value)
CompositionLocalProvider(
LocalTextSelectionColors provides LocalTextSelectionColors.current
) {
BasicTextField(
modifier = Modifier
.testTag(testTag)
.testTag(SEARCH_FIELD_TESTING_TAG)
.width(200.dp)
.padding(start = 20.dp)
.indicatorLine(
enabled = true,
isError = false,
interactionSource = interactionSource,
colors = colors
)
.focusRequester(focusRequester),
value = textFieldValue,
onValueChange = {
@ -110,10 +100,9 @@ fun AppBarTextField(
singleLine = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
isError = false,
placeholder = {
Text(
text = hint,
text = stringResource(R.string.search_label),
color = Color.LightGray
)
},