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 package org.kiwix.kiwixmobile.language
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import org.kiwix.kiwixmobile.cachedComponent import org.kiwix.kiwixmobile.cachedComponent
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.extensions.viewModel import org.kiwix.kiwixmobile.core.extensions.viewModel
import org.kiwix.kiwixmobile.core.main.CoreMainActivity 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.Action
import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel
import javax.inject.Inject 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() { class LanguageFragment : BaseFragment() {
private val languageViewModel by lazy { viewModel<LanguageViewModel>(viewModelFactory) } private val languageViewModel by lazy { viewModel<LanguageViewModel>(viewModelFactory) }
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
val activity = requireActivity() as CoreMainActivity
private lateinit var composeView: ComposeView private lateinit var composeView: ComposeView
private val compositeDisposable = CompositeDisposable() private val compositeDisposable = CompositeDisposable()
override fun inject(baseActivity: BaseActivity) { override fun inject(baseActivity: BaseActivity) {
baseActivity.cachedComponent.inject(this) baseActivity.cachedComponent.inject(this)
} }
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as CoreMainActivity
composeView.setContent { composeView.setContent {
LanguageScreen( var searchText by remember { mutableStateOf("") }
viewModelState = languageViewModel.state, var isSearchActive by remember { mutableStateOf(false) }
onNavigationClick = activity.onBackPressedDispatcher::onBackPressed,
selectLanguageItem = { languageViewModel.actions.offer(Action.Select(it)) }, fun resetSearchState() {
filterText = { languageViewModel.actions.offer(Action.Filter(it)) }, // clears the search text and resets the filter
saveLanguages = { languageViewModel.actions.offer(Action.SaveAll) }, 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( compositeDisposable.add(
languageViewModel.effects.subscribe( 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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,

View File

@ -18,182 +18,63 @@
package org.kiwix.kiwixmobile.language package org.kiwix.kiwixmobile.language
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.LanguageList
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.composables.LoadingIndicator 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
import org.kiwix.kiwixmobile.language.viewmodel.State.Content 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 @Composable
fun LanguageScreen( fun LanguageScreen(
viewModelState: MutableLiveData<State>, languageViewModel: LanguageViewModel
selectLanguageItem: (LanguageItem) -> Unit,
filterText: (String) -> Unit,
onNavigationClick: () -> Unit,
saveLanguages: () -> Unit
) { ) {
val state by viewModelState.observeAsState(State.Loading) val state by languageViewModel.state.observeAsState(State.Loading)
val context = LocalContext.current 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() val listState: LazyListState = rememberLazyListState()
KiwixTheme { Column(modifier = Modifier.fillMaxSize()) {
Scaffold( // spacer to account for top app bar
topBar = { Spacer(modifier = Modifier.height(56.dp))
KiwixAppBar( when (state) {
R.string.select_languages, State.Loading, State.Saving -> {
{ LoadingIndicator()
NavigationIcon( }
iconItem = if (isSearchActive) {
IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack) is Content -> {
} else { val viewItem = (state as Content).viewItems
IconItem.Drawable(
R.drawable.ic_close_white_24dp LaunchedEffect(viewItem) {
) snapshotFlow(listState::firstVisibleItemIndex)
}, .collect {
onClick = { if (listState.firstVisibleItemIndex == 2) {
if (isSearchActive) { listState.animateScrollToItem(0)
isSearchActive = false
searchText = ""
filterText(searchText)
} else {
onNavigationClick()
}
} }
)
},
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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect 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.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppBarTextField( fun AppBarTextField(
value: String, value: String,
hint: String,
testTag: String,
onValueChange: (String) -> Unit onValueChange: (String) -> Unit
) { ) {
val interactionSource = remember(::MutableInteractionSource) val interactionSource = remember(::MutableInteractionSource)
@ -68,29 +67,20 @@ fun AppBarTextField(
unfocusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent,
focusedTextColor = Color.White focusedTextColor = Color.White
) )
val focusRequester = FocusRequester() val focusRequester = FocusRequester()
SideEffect(focusRequester::requestFocus) SideEffect(focusRequester::requestFocus)
var textFieldValue by remember { var textFieldValue by remember {
mutableStateOf(TextFieldValue(value, TextRange(value.length))) mutableStateOf(TextFieldValue(value, TextRange(value.length)))
} }
textFieldValue = textFieldValue.copy(text = value) textFieldValue = textFieldValue.copy(text = value)
CompositionLocalProvider( CompositionLocalProvider(
LocalTextSelectionColors provides LocalTextSelectionColors.current LocalTextSelectionColors provides LocalTextSelectionColors.current
) { ) {
BasicTextField( BasicTextField(
modifier = Modifier modifier = Modifier
.testTag(testTag) .testTag(SEARCH_FIELD_TESTING_TAG)
.width(200.dp) .width(200.dp)
.padding(start = 20.dp) .padding(start = 20.dp)
.indicatorLine(
enabled = true,
isError = false,
interactionSource = interactionSource,
colors = colors
)
.focusRequester(focusRequester), .focusRequester(focusRequester),
value = textFieldValue, value = textFieldValue,
onValueChange = { onValueChange = {
@ -110,10 +100,9 @@ fun AppBarTextField(
singleLine = true, singleLine = true,
visualTransformation = VisualTransformation.None, visualTransformation = VisualTransformation.None,
interactionSource = interactionSource, interactionSource = interactionSource,
isError = false,
placeholder = { placeholder = {
Text( Text(
text = hint, text = stringResource(R.string.search_label),
color = Color.LightGray color = Color.LightGray
) )
}, },