Refactor unit selector screens

decrease duplicate logic in ConverterViewModel
This commit is contained in:
Sad Ellie 2024-02-09 23:25:08 +03:00
parent 9d76c168ec
commit 7cbbb846af
10 changed files with 404 additions and 273 deletions

View File

@ -154,5 +154,45 @@ fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine(
)
}
@Suppress("UNCHECKED_CAST", "UNUSED")
fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
flow7: Flow<T7>,
flow8: Flow<T8>,
flow9: Flow<T9>,
flow10: Flow<T10>,
transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> R,
): Flow<R> =
kotlinx.coroutines.flow.combine(
flow,
flow2,
flow3,
flow4,
flow5,
flow6,
flow7,
flow8,
flow9,
flow10
) { args: Array<*> ->
transform(
args[0] as T1,
args[1] as T2,
args[2] as T3,
args[3] as T4,
args[4] as T5,
args[5] as T6,
args[6] as T7,
args[7] as T8,
args[8] as T9,
args[9] as T10,
)
}
fun <T> Flow<T>.stateIn(scope: CoroutineScope, initialValue: T): StateFlow<T> =
stateIn(scope, SharingStarted.WhileSubscribed(5000L), initialValue)

View File

@ -97,8 +97,8 @@ import java.util.Locale
@Composable
internal fun ConverterRoute(
viewModel: ConverterViewModel = hiltViewModel(),
navigateToLeftScreen: () -> Unit,
navigateToRightScreen: () -> Unit,
navigateToLeftScreen: (uiState: UnitConverterUIState) -> Unit,
navigateToRightScreen: (uiState: UnitConverterUIState) -> Unit,
navigateToMenu: () -> Unit,
navigateToSettings: () -> Unit,
) {
@ -124,8 +124,8 @@ internal fun ConverterRoute(
@Composable
private fun ConverterScreen(
uiState: UnitConverterUIState,
navigateToLeftScreen: () -> Unit,
navigateToRightScreen: () -> Unit,
navigateToLeftScreen: (uiState: UnitConverterUIState) -> Unit,
navigateToRightScreen: (uiState: UnitConverterUIState) -> Unit,
navigateToSettings: () -> Unit,
navigateToMenu: () -> Unit,
swapUnits: () -> Unit,
@ -207,9 +207,9 @@ private fun NumberBase(
onValueChange: (TextFieldValue) -> Unit,
processInput: (String) -> Unit,
deleteDigit: () -> Unit,
navigateToLeftScreen: () -> Unit,
navigateToLeftScreen: (uiState: UnitConverterUIState) -> Unit,
swapUnits: () -> Unit,
navigateToRightScreen: () -> Unit,
navigateToRightScreen: (uiState: UnitConverterUIState) -> Unit,
clearInput: () -> Unit,
) {
PortraitLandscape(
@ -239,8 +239,8 @@ private fun NumberBase(
unitFromLabel = stringResource(uiState.unitFrom.displayName),
unitToLabel = stringResource(uiState.unitTo.displayName),
swapUnits = swapUnits,
navigateToLeftScreen = navigateToLeftScreen,
navigateToRightScreen = navigateToRightScreen
navigateToLeftScreen = { navigateToLeftScreen(uiState) },
navigateToRightScreen = { navigateToRightScreen(uiState) }
)
}
},
@ -263,9 +263,9 @@ private fun Default(
onFocusOnInput2: (Boolean) -> Unit,
processInput: (String) -> Unit,
deleteDigit: () -> Unit,
navigateToLeftScreen: () -> Unit,
navigateToLeftScreen: (uiState: UnitConverterUIState) -> Unit,
swapUnits: () -> Unit,
navigateToRightScreen: () -> Unit,
navigateToRightScreen: (uiState: UnitConverterUIState) -> Unit,
clearInput: () -> Unit,
refreshCurrencyRates: (AbstractUnit) -> Unit,
addBracket: () -> Unit,
@ -327,7 +327,9 @@ private fun Default(
.weight(1f)
) {
ExpressionTextField(
modifier = Modifier.fillMaxWidth().weight(1f),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
value = uiState.input1,
minRatio = 0.7f,
onValueChange = onValueChange,
@ -345,7 +347,9 @@ private fun Default(
.weight(1f)
) {
ExpressionTextField(
modifier = Modifier.fillMaxWidth().weight(1f)
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.onFocusEvent { state -> onFocusOnInput2(state.hasFocus) },
value = uiState.input2,
minRatio = 0.7f,
@ -405,8 +409,8 @@ private fun Default(
unitFromLabel = stringResource(uiState.unitFrom.displayName),
unitToLabel = stringResource(uiState.unitTo.displayName),
swapUnits = swapUnits,
navigateToLeftScreen = navigateToLeftScreen,
navigateToRightScreen = navigateToRightScreen
navigateToLeftScreen = { navigateToLeftScreen(uiState) },
navigateToRightScreen = { navigateToRightScreen(uiState) }
)
}
},

View File

@ -33,7 +33,6 @@ import com.sadellie.unitto.data.common.isExpression
import com.sadellie.unitto.data.common.stateIn
import com.sadellie.unitto.data.converter.UnitID
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.data.model.repository.UnitsRepository
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
import com.sadellie.unitto.data.model.unit.AbstractUnit
@ -45,12 +44,10 @@ import io.github.sadellie.evaluatto.ExpressionException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -74,13 +71,6 @@ internal class ConverterViewModel @Inject constructor(
private val _unitFrom = MutableStateFlow<AbstractUnit?>(null)
private val _unitTo = MutableStateFlow<AbstractUnit?>(null)
private val _leftQuery = MutableStateFlow(TextFieldValue())
private val _leftUnits = MutableStateFlow<Map<UnitGroup, List<AbstractUnit>>>(emptyMap())
private val _leftUnitGroup = MutableStateFlow<UnitGroup?>(null)
private val _rightQuery = MutableStateFlow(TextFieldValue())
private val _rightUnits = MutableStateFlow<Map<UnitGroup, List<AbstractUnit>>>(emptyMap())
private val _currenciesState = MutableStateFlow<CurrencyRateUpdateState>(CurrencyRateUpdateState.Nothing)
private var _loadCurrenciesJob: Job? = null
@ -160,81 +150,6 @@ internal class ConverterViewModel @Inject constructor(
}
.stateIn(viewModelScope, UnitConverterUIState.Loading)
val leftSideUIState = combine(
_unitFrom,
_leftQuery,
_leftUnits,
_leftUnitGroup,
userPrefsRepository.converterPrefs,
unitsRepo.units
) { unitFrom, query, units, unitGroup, prefs, _ ->
unitFrom ?: return@combine LeftSideUIState.Loading
return@combine LeftSideUIState.Ready(
unitFrom = unitFrom,
sorting = prefs.unitConverterSorting,
shownUnitGroups = prefs.shownUnitGroups,
favorites = prefs.unitConverterFavoritesOnly,
query = query,
units = units,
unitGroup = unitGroup
)
}
.mapLatest {
if (it !is LeftSideUIState.Ready) return@mapLatest it
filterUnitsLeft(
query = it.query,
unitGroup = it.unitGroup,
favoritesOnly = it.favorites,
sorting = it.sorting,
shownUnitGroups = it.shownUnitGroups,
)
it
}
.stateIn(viewModelScope, SharingStarted.Lazily, LeftSideUIState.Loading)
val rightSideUIState = combine(
_unitFrom,
_unitTo,
_input1,
_calculation,
_rightQuery,
_rightUnits,
userPrefsRepository.converterPrefs,
_currenciesState,
unitsRepo.units,
) { unitFrom, unitTo, input, calculation, query, units, prefs, currenciesState, _ ->
unitFrom ?: return@combine RightSideUIState.Loading
unitTo ?: return@combine RightSideUIState.Loading
return@combine RightSideUIState.Ready(
unitFrom = unitFrom,
unitTo = unitTo,
sorting = prefs.unitConverterSorting,
favorites = prefs.unitConverterFavoritesOnly,
input = (calculation?.toPlainString() ?: input.text).replace(Token.Operator.minus, "-"),
scale = prefs.precision,
outputFormat = prefs.outputFormat,
formatterSymbols = AllFormatterSymbols.getById(prefs.separator),
currencyRateUpdateState = currenciesState,
query = query,
units = units,
)
}
.mapLatest {
if (it !is RightSideUIState.Ready) return@mapLatest it
filterUnitsRight(
query = it.query,
unitGroup = it.unitFrom.group,
favoritesOnly = it.favorites,
sorting = it.sorting,
)
it
}
.stateIn(viewModelScope, SharingStarted.Lazily, RightSideUIState.Loading)
fun swapUnits() {
_unitFrom
.getAndUpdate { _unitTo.value }
@ -380,56 +295,6 @@ internal class ConverterViewModel @Inject constructor(
}
}
fun queryChangeLeft(query: TextFieldValue) = _leftQuery.update { query }
fun queryChangeRight(query: TextFieldValue) = _rightQuery.update { query }
fun favoritesOnlyChange(enabled: Boolean) = viewModelScope.launch {
userPrefsRepository.updateUnitConverterFavoritesOnly(enabled)
}
fun updateUnitGroupLeft(unitGroup: UnitGroup?) = _leftUnitGroup.update { unitGroup }
fun favoriteUnit(unit: AbstractUnit) = viewModelScope.launch {
unitsRepo.favorite(unit)
}
private fun filterUnitsLeft(
query: TextFieldValue,
unitGroup: UnitGroup?,
favoritesOnly: Boolean,
sorting: UnitsListSorting,
shownUnitGroups: List<UnitGroup>,
) = viewModelScope.launch(Dispatchers.Default) {
_leftUnits.update {
unitsRepo.filterUnits(
query = query.text,
unitGroup = unitGroup,
favoritesOnly = favoritesOnly,
hideBrokenUnits = false,
sorting = sorting,
shownUnitGroups = shownUnitGroups
)
}
}
private fun filterUnitsRight(
query: TextFieldValue,
unitGroup: UnitGroup?,
favoritesOnly: Boolean,
sorting: UnitsListSorting,
) = viewModelScope.launch(Dispatchers.Default) {
_rightUnits.update {
unitsRepo.filterUnits(
query = query.text,
unitGroup = unitGroup,
favoritesOnly = favoritesOnly,
hideBrokenUnits = true,
sorting = sorting,
)
}
}
private fun convertDefault(
unitFrom: DefaultUnit,
unitTo: DefaultUnit,

View File

@ -1,38 +0,0 @@
/*
* Unitto is a calculator for Android
* Copyright (c) 2023-2024 Elshan Agaev
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.converter
import androidx.compose.ui.text.input.TextFieldValue
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.data.model.unit.AbstractUnit
internal sealed class LeftSideUIState {
data object Loading : LeftSideUIState()
data class Ready(
val unitFrom: AbstractUnit,
val query: TextFieldValue,
val units: Map<UnitGroup, List<AbstractUnit>> = emptyMap(),
val favorites: Boolean,
val shownUnitGroups: List<UnitGroup>,
val unitGroup: UnitGroup?,
val sorting: UnitsListSorting,
) : LeftSideUIState()
}

View File

@ -49,31 +49,32 @@ import com.sadellie.unitto.feature.converter.components.UnitsList
import java.math.BigDecimal
@Composable
internal fun LeftSideRoute(
viewModel: ConverterViewModel,
internal fun UnitFromSelectorRoute(
unitSelectorViewModel: UnitSelectorViewModel,
converterViewModel: ConverterViewModel,
navigateUp: () -> Unit,
navigateToUnitGroups: () -> Unit,
) {
when (
val uiState = viewModel.leftSideUIState.collectAsStateWithLifecycle().value
val uiState = unitSelectorViewModel.unitFromUIState.collectAsStateWithLifecycle().value
) {
is LeftSideUIState.Loading -> EmptyScreen()
is LeftSideUIState.Ready -> LeftSideScreen(
is UnitSelectorUIState.UnitFrom -> UnitFromSelectorScreen(
uiState = uiState,
onQueryChange = viewModel::queryChangeLeft,
toggleFavoritesOnly = viewModel::favoritesOnlyChange,
updateUnitFrom = viewModel::updateUnitFrom,
updateUnitGroup = viewModel::updateUnitGroupLeft,
favoriteUnit = viewModel::favoriteUnit,
onQueryChange = unitSelectorViewModel::updateSelectorQuery,
toggleFavoritesOnly = unitSelectorViewModel::updateShowFavoritesOnly,
updateUnitFrom = converterViewModel::updateUnitFrom,
updateUnitGroup = unitSelectorViewModel::updateSelectedUnitGroup,
favoriteUnit = unitSelectorViewModel::favoriteUnit,
navigateUp = navigateUp,
navigateToUnitGroups = navigateToUnitGroups,
)
else -> EmptyScreen()
}
}
@Composable
private fun LeftSideScreen(
uiState: LeftSideUIState.Ready,
private fun UnitFromSelectorScreen(
uiState: UnitSelectorUIState.UnitFrom,
onQueryChange: (TextFieldValue) -> Unit,
toggleFavoritesOnly: (Boolean) -> Unit,
updateUnitFrom: (AbstractUnit) -> Unit,
@ -87,8 +88,6 @@ private fun LeftSideScreen(
val chipsRowLazyListState = rememberLazyListState()
LaunchedEffect(uiState.unitFrom, uiState.shownUnitGroups) {
updateUnitGroup(uiState.unitFrom.group)
kotlin.runCatching {
val groupToSelect = uiState.shownUnitGroups.indexOf(uiState.unitFrom.group)
if (groupToSelect > -1) {
@ -108,8 +107,8 @@ private fun LeftSideScreen(
onQueryChange = onQueryChange,
navigateUp = navigateUp,
trailingIcon = {
FavoritesButton(uiState.favorites) {
toggleFavoritesOnly(!uiState.favorites)
FavoritesButton(uiState.showFavoritesOnly) {
toggleFavoritesOnly(!uiState.showFavoritesOnly)
}
},
scrollBehavior = scrollBehavior
@ -119,7 +118,7 @@ private fun LeftSideScreen(
modifier = Modifier
.padding(start = 8.dp, end = 8.dp, bottom = 4.dp)
.fillMaxWidth(),
chosenUnitGroup = uiState.unitGroup,
chosenUnitGroup = uiState.selectedUnitGroup,
items = uiState.shownUnitGroups,
selectAction = updateUnitGroup,
navigateToSettingsAction = navigateToUnitGroups
@ -130,7 +129,7 @@ private fun LeftSideScreen(
val resources = LocalContext.current.resources
UnitsList(
modifier = Modifier.padding(paddingValues),
groupedUnits = uiState.units,
searchResult = uiState.units,
navigateToUnitGroups = navigateToUnitGroups,
currentUnitId = uiState.unitFrom.id,
supportLabel = { resources.getString(it.shortName) },
@ -146,7 +145,7 @@ private fun LeftSideScreen(
@Preview
@Composable
private fun LeftSideScreenPreview() {
private fun UnitFromSelectorScreenPreview() {
val units: Map<UnitGroup, List<AbstractUnit>> = mapOf(
UnitGroup.LENGTH to listOf(
NormalUnit(UnitID.meter, BigDecimal.valueOf(1.0E+18), UnitGroup.LENGTH, R.string.unit_meter, R.string.unit_meter_short),
@ -159,14 +158,14 @@ private fun LeftSideScreenPreview() {
)
)
LeftSideScreen(
uiState = LeftSideUIState.Ready(
UnitFromSelectorScreen(
uiState = UnitSelectorUIState.UnitFrom(
unitFrom = units.values.first().first(),
units = units,
query = TextFieldValue("test"),
favorites = false,
units = UnitSearchResult.Success(units),
selectedUnitGroup = UnitGroup.SPEED,
shownUnitGroups = UnitGroup.entries,
unitGroup = units.keys.toList().first(),
showFavoritesOnly = false,
sorting = UnitsListSorting.USAGE,
),
onQueryChange = {},

View File

@ -1,6 +1,6 @@
/*
* Unitto is a calculator for Android
* Copyright (c) 2023-2024 Elshan Agaev
* Copyright (c) 2024 Elshan Agaev
*
* 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
@ -24,20 +24,39 @@ import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.UnitsListSorting
import com.sadellie.unitto.data.model.unit.AbstractUnit
internal sealed class RightSideUIState {
data object Loading : RightSideUIState()
internal sealed class UnitSelectorUIState {
data object Loading : UnitSelectorUIState()
data class Ready(
data class UnitFrom(
val query: TextFieldValue,
val unitFrom: AbstractUnit,
val shownUnitGroups: List<UnitGroup>,
val showFavoritesOnly: Boolean,
val units: UnitSearchResult,
val selectedUnitGroup: UnitGroup?,
val sorting: UnitsListSorting,
) : UnitSelectorUIState()
data class UnitTo(
val query: TextFieldValue,
val unitFrom: AbstractUnit,
val unitTo: AbstractUnit,
val query: TextFieldValue,
val units: Map<UnitGroup, List<AbstractUnit>>,
val favorites: Boolean,
val showFavoritesOnly: Boolean,
val units: UnitSearchResult,
val input: String?,
val sorting: UnitsListSorting,
val input: String,
val scale: Int,
val outputFormat: Int,
val formatterSymbols: FormatterSymbols,
val currencyRateUpdateState: CurrencyRateUpdateState,
) : RightSideUIState()
) : UnitSelectorUIState()
}
internal sealed class UnitSearchResult {
data object Empty : UnitSearchResult()
data object Loading : UnitSearchResult()
data class Success(
val units: Map<UnitGroup, List<AbstractUnit>>
) : UnitSearchResult()
}

View File

@ -0,0 +1,149 @@
/*
* Unitto is a calculator for Android
* Copyright (c) 2024 Elshan Agaev
*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.converter
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
import com.sadellie.unitto.data.common.stateIn
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.repository.UnitsRepository
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
import com.sadellie.unitto.data.model.unit.AbstractUnit
import com.sadellie.unitto.feature.converter.navigation.inputArg
import com.sadellie.unitto.feature.converter.navigation.unitFromIdArg
import com.sadellie.unitto.feature.converter.navigation.unitGroupArg
import com.sadellie.unitto.feature.converter.navigation.unitToIdArg
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class UnitSelectorViewModel @Inject constructor(
private val userPrefsRepository: UserPreferencesRepository,
private val unitsRepo: UnitsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _query = MutableStateFlow(TextFieldValue())
private val _searchResults = MutableStateFlow<UnitSearchResult>(UnitSearchResult.Loading)
private val _selectedUnitGroup = MutableStateFlow(savedStateHandle.get<UnitGroup>(unitGroupArg))
private val _unitFromId = savedStateHandle.get<String>(unitFromIdArg)
private val _unitToId = savedStateHandle.get<String>(unitToIdArg)
private val _input = savedStateHandle.get<String>(inputArg)
val unitFromUIState: StateFlow<UnitSelectorUIState> = combine(
_query,
_searchResults,
_selectedUnitGroup,
userPrefsRepository.converterPrefs,
) { query, searchResults, selectedUnitGroup, prefs ->
if (_unitFromId.isNullOrEmpty()) return@combine UnitSelectorUIState.Loading
return@combine UnitSelectorUIState.UnitFrom(
query = query,
unitFrom = unitsRepo.getById(_unitFromId),
shownUnitGroups = prefs.shownUnitGroups,
showFavoritesOnly = prefs.unitConverterFavoritesOnly,
units = searchResults,
selectedUnitGroup = selectedUnitGroup,
sorting = prefs.unitConverterSorting,
)
}
.mapLatest { ui ->
if (ui is UnitSelectorUIState.UnitFrom) {
_searchResults.update {
val result = unitsRepo.filterUnits(
query = ui.query.text,
unitGroup = ui.selectedUnitGroup,
favoritesOnly = ui.showFavoritesOnly,
hideBrokenUnits = false,
sorting = ui.sorting,
shownUnitGroups = ui.shownUnitGroups
)
if (result.isEmpty()) UnitSearchResult.Empty else UnitSearchResult.Success(result)
}
}
ui
}
.stateIn(viewModelScope, UnitSelectorUIState.Loading)
val unitToUIState: StateFlow<UnitSelectorUIState> = combine(
_query,
_searchResults,
userPrefsRepository.converterPrefs,
unitsRepo.units,
) { query, searchResults, prefs, _ ->
if (_unitFromId.isNullOrEmpty()) return@combine UnitSelectorUIState.Loading
if (_unitToId.isNullOrEmpty()) return@combine UnitSelectorUIState.Loading
UnitSelectorUIState.UnitTo(
query = query,
unitFrom = unitsRepo.getById(_unitFromId),
unitTo = unitsRepo.getById(_unitToId),
showFavoritesOnly = prefs.unitConverterFavoritesOnly,
units = searchResults,
input = _input,
sorting = prefs.unitConverterSorting,
scale = prefs.precision,
outputFormat = prefs.outputFormat,
formatterSymbols = AllFormatterSymbols.getById(prefs.separator),
)
}
.mapLatest { ui ->
if (ui is UnitSelectorUIState.UnitTo) {
_searchResults.update {
if (ui.unitFrom.group == UnitGroup.CURRENCY) unitsRepo.updateRates(ui.unitFrom)
val result = unitsRepo.filterUnits(
query = ui.query.text,
unitGroup = ui.unitFrom.group,
favoritesOnly = ui.showFavoritesOnly,
hideBrokenUnits = true,
sorting = ui.sorting,
)
if (result.isEmpty()) UnitSearchResult.Empty else UnitSearchResult.Success(result)
}
}
ui
}
.stateIn(viewModelScope, UnitSelectorUIState.Loading)
fun updateSelectorQuery(value: TextFieldValue) = _query.update { value }
fun updateShowFavoritesOnly(value: Boolean) = viewModelScope.launch {
userPrefsRepository.updateUnitConverterFavoritesOnly(value)
}
fun updateSelectedUnitGroup(value: UnitGroup?) = _selectedUnitGroup.update { value }
fun favoriteUnit(unit: AbstractUnit) = viewModelScope.launch(Dispatchers.IO) {
unitsRepo.favorite(unit)
}
}

View File

@ -47,31 +47,31 @@ import com.sadellie.unitto.feature.converter.components.UnitsList
import java.math.BigDecimal
@Composable
internal fun RightSideRoute(
viewModel: ConverterViewModel,
internal fun UnitToSelectorRoute(
unitSelectorViewModel: UnitSelectorViewModel,
converterViewModel: ConverterViewModel,
navigateUp: () -> Unit,
navigateToUnitGroups: () -> Unit,
) {
when (
val uiState = viewModel.rightSideUIState.collectAsStateWithLifecycle().value
val uiState = unitSelectorViewModel.unitToUIState.collectAsStateWithLifecycle().value
) {
is RightSideUIState.Loading -> EmptyScreen()
is RightSideUIState.Ready ->
RightSideScreen(
is UnitSelectorUIState.UnitTo -> UnitToSelectorScreen(
uiState = uiState,
onQueryChange = viewModel::queryChangeRight,
toggleFavoritesOnly = viewModel::favoritesOnlyChange,
updateUnitTo = viewModel::updateUnitTo,
favoriteUnit = viewModel::favoriteUnit,
onQueryChange = unitSelectorViewModel::updateSelectorQuery,
toggleFavoritesOnly = unitSelectorViewModel::updateShowFavoritesOnly,
updateUnitTo = converterViewModel::updateUnitTo,
favoriteUnit = unitSelectorViewModel::favoriteUnit,
navigateUp = navigateUp,
navigateToUnitGroups = navigateToUnitGroups,
)
else -> EmptyScreen()
}
}
@Composable
private fun RightSideScreen(
uiState: RightSideUIState.Ready,
private fun UnitToSelectorScreen(
uiState: UnitSelectorUIState.UnitTo,
onQueryChange: (TextFieldValue) -> Unit,
toggleFavoritesOnly: (Boolean) -> Unit,
updateUnitTo: (AbstractUnit) -> Unit,
@ -89,8 +89,8 @@ private fun RightSideScreen(
onQueryChange = onQueryChange,
navigateUp = navigateUp,
trailingIcon = {
FavoritesButton(uiState.favorites) {
toggleFavoritesOnly(!uiState.favorites)
FavoritesButton(uiState.showFavoritesOnly) {
toggleFavoritesOnly(!uiState.showFavoritesOnly)
}
},
scrollBehavior = scrollBehavior
@ -100,7 +100,7 @@ private fun RightSideScreen(
val resources = LocalContext.current.resources
UnitsList(
modifier = Modifier.padding(paddingValues),
groupedUnits = uiState.units,
searchResult = uiState.units,
navigateToUnitGroups = navigateToUnitGroups,
currentUnitId = uiState.unitTo.id,
supportLabel = {
@ -112,7 +112,6 @@ private fun RightSideScreen(
scale = uiState.scale,
outputFormat = uiState.outputFormat,
formatterSymbols = uiState.formatterSymbols,
readyCurrencies = uiState.currencyRateUpdateState is CurrencyRateUpdateState.Ready,
)
},
onClick = {
@ -128,15 +127,13 @@ private fun RightSideScreen(
private fun formatUnitToSupportLabel(
unitFrom: AbstractUnit?,
unitTo: AbstractUnit?,
input: String,
input: String?,
shortName: String,
scale: Int,
outputFormat: Int,
formatterSymbols: FormatterSymbols,
readyCurrencies: Boolean,
): String {
if ((unitFrom?.group == UnitGroup.CURRENCY) and !readyCurrencies) return shortName
if (input.isEmpty()) return shortName
if (input.isNullOrEmpty()) return shortName
try {
if ((unitFrom is DefaultUnit) and (unitTo is DefaultUnit)) {
@ -168,7 +165,7 @@ private fun formatUnitToSupportLabel(
@Preview
@Composable
private fun RightSideScreenPreview() {
private fun UnitToSelectorPreview() {
val units: Map<UnitGroup, List<AbstractUnit>> = mapOf(
UnitGroup.LENGTH to listOf(
NormalUnit(UnitID.meter, BigDecimal.valueOf(1.0E+18), UnitGroup.LENGTH, R.string.unit_meter, R.string.unit_meter_short),
@ -181,19 +178,18 @@ private fun RightSideScreenPreview() {
)
)
RightSideScreen(
uiState = RightSideUIState.Ready(
UnitToSelectorScreen(
uiState = UnitSelectorUIState.UnitTo(
unitFrom = units.values.first().first(),
units = units,
query = TextFieldValue(),
favorites = false,
unitTo = units.values.first().first(),
query = TextFieldValue("test"),
units = UnitSearchResult.Success(units),
showFavoritesOnly = false,
sorting = UnitsListSorting.USAGE,
unitTo = units.values.first()[1],
input = "100",
scale = 3,
outputFormat = OutputFormat.PLAIN,
formatterSymbols = FormatterSymbols.Spaces,
currencyRateUpdateState = CurrencyRateUpdateState.Nothing
),
onQueryChange = {},
toggleFavoritesOnly = {},

View File

@ -33,12 +33,13 @@ import com.sadellie.unitto.data.converter.UnitID
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.data.model.unit.AbstractUnit
import com.sadellie.unitto.data.model.unit.NormalUnit
import com.sadellie.unitto.feature.converter.UnitSearchResult
import java.math.BigDecimal
@Composable
internal fun UnitsList(
modifier: Modifier,
groupedUnits: Map<UnitGroup, List<AbstractUnit>>,
searchResult: UnitSearchResult,
navigateToUnitGroups: () -> Unit,
currentUnitId: String,
supportLabel: (AbstractUnit) -> String,
@ -47,14 +48,14 @@ internal fun UnitsList(
) {
Crossfade(
modifier = modifier,
targetState = groupedUnits.isNotEmpty(),
targetState = searchResult,
label = "Units list"
) { hasUnits ->
when (hasUnits) {
true -> LazyColumn(
) { result ->
when (result) {
is UnitSearchResult.Success -> LazyColumn(
modifier = Modifier.fillMaxSize()
) {
groupedUnits.forEach { (group, units) ->
result.units.forEach { (group, units) ->
item(group.name) {
UnitGroupHeader(Modifier.animateItemPlacement(), group)
}
@ -73,11 +74,13 @@ internal fun UnitsList(
}
}
false -> SearchPlaceholder(
UnitSearchResult.Empty -> SearchPlaceholder(
onButtonClick = navigateToUnitGroups,
supportText = stringResource(R.string.converter_no_results_support),
buttonLabel = stringResource(R.string.open_settings_label)
)
UnitSearchResult.Loading -> Unit
}
}
}
@ -100,7 +103,7 @@ private fun PreviewUnitsList() {
UnitsList(
modifier = Modifier.fillMaxSize(),
groupedUnits = groupedUnits,
searchResult = UnitSearchResult.Success(units = groupedUnits),
navigateToUnitGroups = {},
currentUnitId = UnitID.mile,
supportLabel = { resources.getString(it.shortName) },

View File

@ -22,19 +22,42 @@ import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.sadellie.unitto.core.ui.model.DrawerItem
import com.sadellie.unitto.core.ui.unittoComposable
import com.sadellie.unitto.core.ui.unittoNavigation
import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.feature.converter.ConverterRoute
import com.sadellie.unitto.feature.converter.ConverterViewModel
import com.sadellie.unitto.feature.converter.LeftSideRoute
import com.sadellie.unitto.feature.converter.RightSideRoute
import com.sadellie.unitto.feature.converter.CurrencyRateUpdateState
import com.sadellie.unitto.feature.converter.UnitConverterUIState
import com.sadellie.unitto.feature.converter.UnitFromSelectorRoute
import com.sadellie.unitto.feature.converter.UnitToSelectorRoute
private val graph = DrawerItem.Converter.graph
private val start = DrawerItem.Converter.start
private const val LEFT = "left"
private const val RIGHT = "right"
private const val UNIT_FROM = "unitFromSelector"
private const val UNIT_TO = "unitToSelector"
internal const val unitGroupArg = "unitGroupArg"
internal const val unitFromIdArg = "unitFromId"
internal const val unitToIdArg = "unitToIdArg"
internal const val inputArg = "inputArg"
private const val UNIT_FROM_ROUTE = "$UNIT_FROM/{$unitFromIdArg}/{$unitGroupArg}"
private const val UNIT_TO_ROUTE = "$UNIT_TO/{$unitFromIdArg}/{$unitToIdArg}/{$inputArg}"
private fun NavHostController.navigateLeft(
unitFromId: String,
unitGroup: UnitGroup,
) = navigate("$UNIT_FROM/$unitFromId/$unitGroup")
private fun NavHostController.navigateRight(
unitFromId: String,
unitToId: String,
input: String?,
) = navigate("$UNIT_TO/$unitFromId/$unitToId/$input")
fun NavGraphBuilder.converterGraph(
openDrawer: () -> Unit,
@ -58,36 +81,107 @@ fun NavGraphBuilder.converterGraph(
ConverterRoute(
viewModel = parentViewModel,
navigateToLeftScreen = { navController.navigate(LEFT) },
navigateToRightScreen = { navController.navigate(RIGHT) },
// Navigation logic is here, but should actually be in ConverterScreen
navigateToLeftScreen = { uiState: UnitConverterUIState ->
when (uiState) {
is UnitConverterUIState.Default -> navController
.navigateLeft(uiState.unitFrom.id, uiState.unitFrom.group)
is UnitConverterUIState.NumberBase -> navController
.navigateLeft(uiState.unitFrom.id, uiState.unitFrom.group)
else -> Unit
}
},
navigateToRightScreen = { uiState: UnitConverterUIState ->
when (uiState) {
is UnitConverterUIState.Default -> {
// Don't allow converting if still loading currencies
val convertingCurrencies = uiState.unitFrom.group == UnitGroup.CURRENCY
val currenciesReady =
uiState.currencyRateUpdateState is CurrencyRateUpdateState.Ready
val input: String? = if (convertingCurrencies and !currenciesReady) {
null
} else {
(uiState.calculation?.toPlainString() ?: uiState.input1.text)
.ifEmpty { null }
}
navController.navigateRight(
uiState.unitFrom.id,
uiState.unitTo.id,
input
)
}
is UnitConverterUIState.NumberBase -> {
val input = uiState.input.text.ifEmpty { null }
navController.navigateRight(
uiState.unitFrom.id,
uiState.unitTo.id,
input
)
}
UnitConverterUIState.Loading -> Unit
}
},
navigateToSettings = navigateToSettings,
navigateToMenu = openDrawer
)
}
unittoComposable(LEFT) { backStackEntry ->
unittoComposable(
route = UNIT_FROM_ROUTE,
arguments = listOf(
navArgument(unitFromIdArg) {
type = NavType.StringType
},
navArgument(unitGroupArg) {
type = NavType.EnumType(UnitGroup::class.java)
},
)
) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry(graph)
}
val parentViewModel = hiltViewModel<ConverterViewModel>(parentEntry)
LeftSideRoute(
viewModel = parentViewModel,
UnitFromSelectorRoute(
unitSelectorViewModel = hiltViewModel(),
converterViewModel = parentViewModel,
navigateUp = navController::navigateUp,
navigateToUnitGroups = navigateToUnitGroups
)
}
unittoComposable(RIGHT) { backStackEntry ->
unittoComposable(
route = UNIT_TO_ROUTE,
arguments = listOf(
navArgument(unitFromIdArg) {
type = NavType.StringType
},
navArgument(unitToIdArg) {
type = NavType.StringType
},
navArgument(inputArg) {
type = NavType.StringType
nullable = true
defaultValue = null
},
)
) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry(graph)
}
val parentViewModel = hiltViewModel<ConverterViewModel>(parentEntry)
RightSideRoute(
viewModel = parentViewModel,
UnitToSelectorRoute(
unitSelectorViewModel = hiltViewModel(),
converterViewModel = parentViewModel,
navigateUp = navController::navigateUp,
navigateToUnitGroups = navigateToUnitGroups
)