feat: language fragment to jetpack compose

- added inner padding to scaffolding to account for topAppBar.
- moved AppBarTextField to ui/components so it can be re-used.
- shifted hardcoded values to ComposeDimens.
- Added ContentLoadingProgressBar modified it to support
Indeterminate mode
- further decoupling of composable functions
This commit is contained in:
jaskaran 2025-03-21 13:11:09 +05:30 committed by MohitMaliFtechiz
parent 583cc7241e
commit 695446e291
8 changed files with 144 additions and 175 deletions

View File

@ -18,14 +18,13 @@
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.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -39,12 +38,10 @@ 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.components.NavigationIcon
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme 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
@ -64,7 +61,6 @@ class LanguageFragment : BaseFragment() {
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 val activity = requireActivity() as CoreMainActivity
@ -79,12 +75,33 @@ class LanguageFragment : BaseFragment() {
} }
KiwixTheme { KiwixTheme {
Scaffold(topBar = { LanguageScreen(
KiwixAppBar( searchText = searchText,
titleId = R.string.select_languages,
navigationIcon = {
AppBarNavigationIcon(
isSearchActive = isSearchActive, isSearchActive = isSearchActive,
appBarTextFieldTestTag = SEARCH_FIELD_TESTING_TAG,
languageViewModel = languageViewModel,
actionMenuItemList = appBarActionMenuList(
searchText = searchText,
isSearchActive = isSearchActive,
onSearchClick = { isSearchActive = true },
onClearClick = { resetSearchState() },
onSaveClick = {
languageViewModel.actions.offer(Action.SaveAll)
}
),
onAppBarValueChange = {
searchText = it
languageViewModel.actions.offer(Action.Filter(it))
},
content = {
NavigationIcon(
iconItem = if (isSearchActive) {
IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack)
} else {
IconItem.Drawable(
R.drawable.ic_close_white_24dp
)
},
onClick = { onClick = {
if (isSearchActive) { if (isSearchActive) {
isSearchActive = false isSearchActive = false
@ -94,40 +111,9 @@ class LanguageFragment : BaseFragment() {
} }
} }
) )
},
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) compositeAdd(activity)
} }

View File

@ -18,22 +18,31 @@
package org.kiwix.kiwixmobile.language package org.kiwix.kiwixmobile.language
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding
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.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.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.ui.components.AppBarTextField
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.language.composables.LanguageList import org.kiwix.kiwixmobile.language.composables.LanguageList
import org.kiwix.kiwixmobile.language.composables.LoadingIndicator
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 org.kiwix.kiwixmobile.language.viewmodel.State import org.kiwix.kiwixmobile.language.viewmodel.State
@ -41,18 +50,49 @@ import org.kiwix.kiwixmobile.language.viewmodel.State.Content
@Composable @Composable
fun LanguageScreen( fun LanguageScreen(
languageViewModel: LanguageViewModel searchText: String,
isSearchActive: Boolean,
languageViewModel: LanguageViewModel,
actionMenuItemList: List<ActionMenuItem>,
onAppBarValueChange: (String) -> Unit,
appBarTextFieldTestTag: String,
content: @Composable() () -> Unit,
) { ) {
val state by languageViewModel.state.observeAsState(State.Loading) val state by languageViewModel.state.observeAsState(State.Loading)
val context = LocalContext.current
val listState: LazyListState = rememberLazyListState() val listState: LazyListState = rememberLazyListState()
val context = LocalContext.current
Column(modifier = Modifier.fillMaxSize()) { Scaffold(topBar = {
// spacer to account for top app bar KiwixAppBar(
Spacer(modifier = Modifier.height(56.dp)) titleId = R.string.select_languages,
navigationIcon = content,
actionMenuItems = actionMenuItemList,
searchBar = if (isSearchActive) {
{
AppBarTextField(
value = searchText,
testTag = appBarTextFieldTestTag,
onValueChange = onAppBarValueChange
)
}
} else {
null
}
)
}) { innerPadding ->
Column(
modifier = Modifier.fillMaxSize()
// setting bottom padding to zero to avoid accounting for Bottom bar
.padding(
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(LocalLayoutDirection.current),
end = innerPadding.calculateEndPadding(LocalLayoutDirection.current),
bottom = 0.dp
)
) {
when (state) { when (state) {
State.Loading, State.Saving -> { State.Loading, State.Saving -> {
LoadingIndicator() LoadingScreen()
} }
is Content -> { is Content -> {
@ -66,6 +106,7 @@ fun LanguageScreen(
} }
} }
} }
LanguageList( LanguageList(
context = context, context = context,
listState = listState, listState = listState,
@ -78,3 +119,14 @@ fun LanguageScreen(
} }
} }
} }
}
@Composable
fun LoadingScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
ContentLoadingProgressBar()
}
}

View File

@ -1,43 +0,0 @@
/*
* 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

@ -24,9 +24,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
@Composable @Composable
fun HeaderText( fun HeaderText(
@ -40,8 +39,8 @@ fun HeaderText(
else -> "" else -> ""
}, },
modifier = modifier modifier = modifier
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = ComposeDimens.SIXTEEN_DP, vertical = ComposeDimens.EIGHT_DP),
fontSize = 16.sp, fontSize = ComposeDimens.FOURTEEN_SP,
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )

View File

@ -38,6 +38,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.ComposeDimens
import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem
const val LANGUAGE_ITEM_CHECKBOX_TESTING_TAG = "languageItemCheckboxTestingTag" const val LANGUAGE_ITEM_CHECKBOX_TESTING_TAG = "languageItemCheckboxTestingTag"
@ -87,7 +88,7 @@ fun LanguageItemRow(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
text = stringResource(R.string.books_count, language.occurencesOfLanguage), text = stringResource(R.string.books_count, language.occurencesOfLanguage),
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(ComposeDimens.SIXTEEN_DP),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondary color = MaterialTheme.colorScheme.onSecondary
) )

View File

@ -1,36 +0,0 @@
/*
* 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.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun LoadingIndicator() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}

View File

@ -16,7 +16,7 @@
* *
*/ */
package org.kiwix.kiwixmobile.language.composables package org.kiwix.kiwixmobile.core.ui.components
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@ -48,12 +48,12 @@ 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.core.R
import org.kiwix.kiwixmobile.language.SEARCH_FIELD_TESTING_TAG
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppBarTextField( fun AppBarTextField(
value: String, value: String,
testTag: String = "",
onValueChange: (String) -> Unit onValueChange: (String) -> Unit
) { ) {
val interactionSource = remember(::MutableInteractionSource) val interactionSource = remember(::MutableInteractionSource)
@ -78,7 +78,7 @@ fun AppBarTextField(
) { ) {
BasicTextField( BasicTextField(
modifier = Modifier modifier = Modifier
.testTag(SEARCH_FIELD_TESTING_TAG) .testTag(testTag)
.width(200.dp) .width(200.dp)
.padding(start = 20.dp) .padding(start = 20.dp)
.focusRequester(focusRequester), .focusRequester(focusRequester),

View File

@ -42,12 +42,22 @@ fun ContentLoadingProgressBar(
) { ) {
when (progressBarStyle) { when (progressBarStyle) {
ProgressBarStyle.CIRCLE -> { ProgressBarStyle.CIRCLE -> {
if (progress == ZERO) {
// Indeterminate mode - will spin continuously
CircularProgressIndicator( CircularProgressIndicator(
modifier = modifier, modifier = modifier,
color = progressBarColor
)
} else {
// Determinate mode - shows specific progress
CircularProgressIndicator(
modifier = modifier,
progress = { progress.toFloat() / HUNDERED },
color = progressBarColor, color = progressBarColor,
trackColor = progressBarTrackColor trackColor = progressBarTrackColor
) )
} }
}
ProgressBarStyle.HORIZONTAL -> { ProgressBarStyle.HORIZONTAL -> {
LinearProgressIndicator( LinearProgressIndicator(