Better (?) MutableStateFlows approach

This commit is contained in:
Sad Ellie 2022-09-03 13:01:06 +03:00
parent 0ffec5e15a
commit 66227f4ed7
6 changed files with 151 additions and 125 deletions

View File

@ -26,6 +26,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@ -41,8 +42,8 @@ import com.sadellie.unitto.data.NavRoutes.SETTINGS_GRAPH
import com.sadellie.unitto.data.NavRoutes.SETTINGS_SCREEN
import com.sadellie.unitto.data.NavRoutes.THEMES_SCREEN
import com.sadellie.unitto.data.NavRoutes.UNIT_GROUPS_SCREEN
import com.sadellie.unitto.screens.main.MainViewModel
import com.sadellie.unitto.screens.main.MainScreen
import com.sadellie.unitto.screens.main.MainViewModel
import com.sadellie.unitto.screens.second.LeftSideScreen
import com.sadellie.unitto.screens.second.RightSideScreen
import com.sadellie.unitto.screens.second.SecondViewModel
@ -67,15 +68,15 @@ class MainActivity : ComponentActivity() {
setContent {
val settingsViewModel: SettingsViewModel = hiltViewModel()
val userPrefs = settingsViewModel.userPrefs
val userPrefs = settingsViewModel.userPrefs.collectAsStateWithLifecycle()
val themmoController = rememberThemmoController(
lightColorScheme = LightThemeColors,
darkColorScheme = DarkThemeColors,
// Anything below will not called if theming mode is still loading from DataStore
themingMode = userPrefs.themingMode ?: return@setContent,
dynamicThemeEnabled = userPrefs.enableDynamicTheme,
amoledThemeEnabled = userPrefs.enableAmoledTheme
themingMode = userPrefs.value.themingMode ?: return@setContent,
dynamicThemeEnabled = userPrefs.value.enableDynamicTheme,
amoledThemeEnabled = userPrefs.value.enableAmoledTheme
)
val navController = rememberNavController()
val sysUiController = rememberSystemUiController()

View File

@ -28,6 +28,8 @@ import com.sadellie.unitto.data.KEY_DOT
import com.sadellie.unitto.data.KEY_E
import com.sadellie.unitto.data.preferences.Separator
import com.sadellie.unitto.data.units.AbstractUnit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.NumberFormat
@ -222,3 +224,27 @@ fun Sequence<AbstractUnit>.sortByLev(stringA: String): Sequence<AbstractUnit> {
.map { it.first }
.asSequence()
}
@Suppress("UNCHECKED_CAST")
fun <T1, T2, T3, T4, T5, T6, T7, T8, 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>,
transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { 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
)
}

View File

@ -19,7 +19,6 @@
package com.sadellie.unitto.screens.main
import android.app.Application
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -40,12 +39,13 @@ import com.sadellie.unitto.data.units.database.MyBasedUnit
import com.sadellie.unitto.data.units.database.MyBasedUnitsRepository
import com.sadellie.unitto.data.units.remote.CurrencyApi
import com.sadellie.unitto.data.units.remote.CurrencyUnitResponse
import com.sadellie.unitto.screens.combine
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
@ -58,12 +58,39 @@ class MainViewModel @Inject constructor(
private val application: Application,
private val allUnitsRepository: AllUnitsRepository
) : ViewModel() {
private var userPrefs = UserPreferences()
/**
* UI state
*/
private val _mainUIState = MutableStateFlow(MainScreenUIState())
private val _inputValue: MutableStateFlow<String> = MutableStateFlow(KEY_0)
private val _deleteButtonEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _dotButtonEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
private val _negateButtonEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _isLoadingDatabase: MutableStateFlow<Boolean> = MutableStateFlow(true)
private val _isLoadingNetwork: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _showError: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _userPrefs = userPrefsRepository.userPreferencesFlow
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UserPreferences())
val mainFlow = combine(
_inputValue,
_deleteButtonEnabled,
_dotButtonEnabled,
_negateButtonEnabled,
_isLoadingDatabase,
_isLoadingNetwork,
_showError,
_userPrefs
) { inputValue, deleteButtonEnabled, dotButtonEnabled, negateButtonEnabled, isLoadingDatabase, isLoadingNetwork, showError, _ ->
return@combine MainScreenUIState(
inputValue = inputValue,
resultValue = convertValue(),
deleteButtonEnabled = deleteButtonEnabled,
dotButtonEnabled = dotButtonEnabled,
negateButtonEnabled = negateButtonEnabled,
isLoadingDatabase = isLoadingDatabase,
isLoadingNetwork = isLoadingNetwork,
showError = showError
)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainScreenUIState())
/**
* Unit we converting from (left side)
@ -77,27 +104,16 @@ class MainViewModel @Inject constructor(
var unitTo: AbstractUnit by mutableStateOf(allUnitsRepository.getById(MyUnitIDS.mile))
private set
val mainFlow = combine(_mainUIState, userPrefsRepository.userPreferencesFlow) { UIState, prefs ->
userPrefs = prefs
convertValue()
return@combine UIState
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MainScreenUIState()
)
/**
* This function takes local variables, converts values and then causes the UI to update
*/
private fun convertValue() {
private fun convertValue(): String {
// Converting value using a specified precision
val convertedValue: BigDecimal =
unitFrom.convert(
unitTo,
_mainUIState.value.inputValue.toBigDecimal(),
userPrefs.digitsPrecision
_inputValue.value.toBigDecimal(),
_userPrefs.value.digitsPrecision
)
/**
@ -107,20 +123,20 @@ class MainViewModel @Inject constructor(
*/
val resultValue =
if (convertedValue == BigDecimal.ZERO.setScale(
userPrefs.digitsPrecision,
_userPrefs.value.digitsPrecision,
RoundingMode.HALF_EVEN
)
) {
KEY_0
} else {
// Setting result value using a specified OutputFormat
when (userPrefs.outputFormat) {
when (_userPrefs.value.outputFormat) {
OutputFormat.ALLOW_ENGINEERING -> convertedValue.toString()
OutputFormat.FORCE_ENGINEERING -> convertedValue.toEngineeringString()
else -> convertedValue.toPlainString()
}
}
_mainUIState.value = _mainUIState.value.copy(resultValue = resultValue)
return resultValue
}
/**
@ -133,16 +149,10 @@ class MainViewModel @Inject constructor(
unitFrom = clickedUnit
// Now we check for negate button
_mainUIState.value =
_mainUIState.value.copy(negateButtonEnabled = clickedUnit.group.canNegate)
_negateButtonEnabled.update { clickedUnit.group.canNegate }
// Now we change to positive if the group we switched to supports negate
if (!clickedUnit.group.canNegate) {
_mainUIState.value =
_mainUIState.value.copy(
inputValue = _mainUIState.value.inputValue.removePrefix(
KEY_MINUS
)
)
_inputValue.update { _inputValue.value.removePrefix(KEY_MINUS) }
}
// Now setting up right unit (pair for the left one)
@ -157,8 +167,6 @@ class MainViewModel @Inject constructor(
incrementCounter(clickedUnit)
// Currencies require us to get data from the internet
updateCurrenciesBasicUnits()
// We can't call outside of this block. It will set precision to 0 in that case
convertValue()
// Saving latest pair
saveLatestPairOfUnits()
}
@ -190,9 +198,6 @@ class MainViewModel @Inject constructor(
// Saving latest pair
saveLatestPairOfUnits()
}
// Changed units, now we can convert
convertValue()
}
private suspend fun incrementCounter(unit: AbstractUnit) {
@ -213,12 +218,13 @@ class MainViewModel @Inject constructor(
*/
private suspend fun updateCurrenciesBasicUnits() {
// Resetting error and network loading states in case we are not gonna do anything below
_mainUIState.value = _mainUIState.value.copy(isLoadingNetwork = false, showError = false)
_isLoadingNetwork.update { false }
_showError.update { false }
// We update currencies only when needed
if (unitFrom.group != UnitGroup.CURRENCY) return
// Starting to load stuff
_mainUIState.value = _mainUIState.value.copy(isLoadingNetwork = true)
_isLoadingNetwork.update { true }
try {
val pairs: CurrencyUnitResponse =
@ -233,10 +239,10 @@ class MainViewModel @Inject constructor(
FirebaseHelper().recordException(e)
}
}
_mainUIState.value = _mainUIState.value.copy(showError = true)
_showError.update { true }
} finally {
// Loaded
_mainUIState.value = _mainUIState.value.copy(isLoadingNetwork = false)
_isLoadingNetwork.update { false }
}
}
@ -251,8 +257,6 @@ class MainViewModel @Inject constructor(
updateCurrenciesBasicUnits()
saveLatestPairOfUnits()
}
// Swapped, can convert now
convertValue()
}
/**
@ -266,17 +270,14 @@ class MainViewModel @Inject constructor(
// Here we add a dot to input
// Disabling dot button to avoid multiple dots in input value
// Enabling delete button to so that we can delete this dot from input
_mainUIState.value = _mainUIState.value.copy(
inputValue = _mainUIState.value.inputValue + digitToAdd,
dotButtonEnabled = false,
deleteButtonEnabled = true
)
_inputValue.update { _inputValue.value + digitToAdd }
_dotButtonEnabled.update { false }
_deleteButtonEnabled.update { true }
}
KEY_0 -> {
// We shouldn't add zero to another zero in input, i.e. 00
if (_mainUIState.value.inputValue != KEY_0) {
_mainUIState.value =
_mainUIState.value.copy(inputValue = _mainUIState.value.inputValue + digitToAdd)
if (_inputValue.value != KEY_0) {
_inputValue.update { _inputValue.value + digitToAdd }
}
}
else -> {
@ -285,13 +286,12 @@ class MainViewModel @Inject constructor(
When there is just a zero, we should replace it with the digit we want to add,
avoids input to be like 03 (with this check it will be just 3)
*/
_mainUIState.value = _mainUIState.value.copy(
inputValue = if (_mainUIState.value.inputValue == KEY_0) digitToAdd else _mainUIState.value.inputValue + digitToAdd,
deleteButtonEnabled = true
)
_inputValue.update {
if (_inputValue.value == KEY_0) digitToAdd else _inputValue.value + digitToAdd
}
_deleteButtonEnabled.update { true }
}
}
convertValue()
}
/**
@ -300,13 +300,12 @@ class MainViewModel @Inject constructor(
fun deleteDigit() {
// Last symbol is a dot
// We enable DOT button
if (_mainUIState.value.inputValue.endsWith(KEY_DOT)) {
_mainUIState.value = _mainUIState.value.copy(dotButtonEnabled = true)
if (_inputValue.value.endsWith(KEY_DOT)) {
_dotButtonEnabled.update { true }
}
// Deleting last symbol
_mainUIState.value =
_mainUIState.value.copy(inputValue = _mainUIState.value.inputValue.dropLast(1))
var inputToSet = _inputValue.value.dropLast(1)
/*
Now we check what we have left
@ -316,42 +315,37 @@ class MainViewModel @Inject constructor(
Skipping this block means that we are left we acceptable value, i.e. 123.03
*/
if (
_mainUIState.value.inputValue in listOf(String(), KEY_MINUS, KEY_0)
inputToSet in listOf(String(), KEY_MINUS, KEY_0)
) {
_mainUIState.value =
_mainUIState.value.copy(deleteButtonEnabled = false, inputValue = KEY_0)
_deleteButtonEnabled.update { false }
inputToSet = KEY_0
}
// We are sure that input has acceptable value, so we convert it
convertValue()
_inputValue.update { inputToSet }
}
/**
* Clears input value and sets it to default (ZERO)
*/
fun clearInput() {
_mainUIState.value = _mainUIState.value.copy(
inputValue = KEY_0,
deleteButtonEnabled = false,
dotButtonEnabled = true
)
convertValue()
_inputValue.update { KEY_0 }
_deleteButtonEnabled.update { false }
_dotButtonEnabled.update { true }
}
/**
* Changes input from positive to negative and vice versa
*/
fun negateInput() {
_mainUIState.value = _mainUIState.value.copy(
inputValue = if (_mainUIState.value.inputValue.getOrNull(0) != KEY_MINUS.single()) {
_inputValue.update {
if (_inputValue.value.getOrNull(0) != KEY_MINUS.single()) {
// If input doesn't have minus at the beginning, we give it to it
KEY_MINUS + _mainUIState.value.inputValue
KEY_MINUS + _inputValue.value
} else {
// Input has minus, meaning we need to remove it
_mainUIState.value.inputValue.removePrefix(KEY_MINUS)
_inputValue.value.removePrefix(KEY_MINUS)
}
)
convertValue()
}
}
/**
@ -363,34 +357,29 @@ class MainViewModel @Inject constructor(
init {
viewModelScope.launch {
userPrefs = userPrefsRepository.userPreferencesFlow.first()
val initialUserPrefs = userPrefsRepository.userPreferencesFlow.first()
// First we load latest pair of units
unitFrom = try {
allUnitsRepository.getById(userPrefs.latestLeftSideUnit)
allUnitsRepository.getById(initialUserPrefs.latestLeftSideUnit)
} catch (e: java.util.NoSuchElementException) {
Log.w("MainViewModel", "No unit with the given unitId")
allUnitsRepository.getById(MyUnitIDS.kilometer)
}
unitTo = try {
allUnitsRepository.getById(userPrefs.latestRightSideUnit)
allUnitsRepository.getById(initialUserPrefs.latestRightSideUnit)
} catch (e: java.util.NoSuchElementException) {
Log.w("MainViewModel", "No unit with the given unitId")
allUnitsRepository.getById(MyUnitIDS.mile)
}
_mainUIState.value =
_mainUIState.value.copy(negateButtonEnabled = unitFrom.group.canNegate)
// Now we load units data from database
val allBasedUnits = basedUnitRepository.getAll()
allUnitsRepository.loadFromDatabase(application, allBasedUnits)
// User is free to convert values and units on units screen can be sorted properly
_mainUIState.value = _mainUIState.value.copy(isLoadingDatabase = false)
_negateButtonEnabled.update { unitFrom.group.canNegate }
_isLoadingDatabase.update { false }
updateCurrenciesBasicUnits()
convertValue()
}
}
}

View File

@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ -43,25 +44,35 @@ class SecondViewModel @Inject constructor(
unitGroupsRepository: UnitGroupsRepository
) : ViewModel() {
private val _uiStateFlow = MutableStateFlow(SecondScreenUIState())
private val _favoritesOnly = MutableStateFlow(false)
private val _unitsToShow = MutableStateFlow(emptyMap<UnitGroup, List<AbstractUnit>>())
private val _searchQuery = MutableStateFlow("")
private val _chosenUnitGroup: MutableStateFlow<UnitGroup?> = MutableStateFlow(null)
private val _shownUnitGroups = unitGroupsRepository.shownUnitGroups
val mainFlow = combine(_uiStateFlow, unitGroupsRepository.shownUnitGroups) { uiState, shown ->
val newState = uiState.copy(shownUnitGroups = shown)
_uiStateFlow.value = newState
return@combine newState
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = SecondScreenUIState()
val mainFlow = combine(
_favoritesOnly,
_unitsToShow,
_searchQuery,
_chosenUnitGroup,
_shownUnitGroups
) { favoritesOnly, unitsToShow, searchQuery, chosenUnitGroup, shownUnitGroups ->
return@combine SecondScreenUIState(
favoritesOnly = favoritesOnly,
unitsToShow = unitsToShow,
searchQuery = searchQuery,
chosenUnitGroup = chosenUnitGroup,
shownUnitGroups = shownUnitGroups
)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SecondScreenUIState())
fun toggleFavoritesOnly() {
_uiStateFlow.value = _uiStateFlow.value.copy(favoritesOnly = !_uiStateFlow.value.favoritesOnly)
_favoritesOnly.update { !_favoritesOnly.value }
}
fun onSearchQueryChange(newValue: String) {
_uiStateFlow.value = _uiStateFlow.value.copy(searchQuery = newValue)
_searchQuery.update { newValue }
}
/**
@ -70,7 +81,7 @@ class SecondViewModel @Inject constructor(
* @param unitGroup Chip to change to.
*/
fun setSelectedChip(unitGroup: UnitGroup) {
_uiStateFlow.value = _uiStateFlow.value.copy(chosenUnitGroup = unitGroup)
_chosenUnitGroup.update { unitGroup }
}
/**
@ -82,8 +93,8 @@ class SecondViewModel @Inject constructor(
* @param unitGroup [UnitGroup], currently selected chip.
*/
fun toggleSelectedChip(unitGroup: UnitGroup) {
val newUnitGroup = if (_uiStateFlow.value.chosenUnitGroup == unitGroup) null else unitGroup
_uiStateFlow.value = _uiStateFlow.value.copy(chosenUnitGroup = newUnitGroup)
val newUnitGroup = if (_chosenUnitGroup.value == unitGroup) null else unitGroup
_chosenUnitGroup.update { newUnitGroup }
}
/**
@ -101,13 +112,13 @@ class SecondViewModel @Inject constructor(
withContext(Dispatchers.Default) {
val unitsToShow = allUnitsRepository.filterUnits(
hideBrokenCurrencies = hideBrokenCurrencies,
chosenUnitGroup = _uiStateFlow.value.chosenUnitGroup,
favoritesOnly = _uiStateFlow.value.favoritesOnly,
searchQuery = _uiStateFlow.value.searchQuery,
allUnitsGroups = _uiStateFlow.value.shownUnitGroups
chosenUnitGroup = _chosenUnitGroup.value,
favoritesOnly = _favoritesOnly.value,
searchQuery = _searchQuery.value,
allUnitsGroups = _shownUnitGroups.value
)
_uiStateFlow.value = _uiStateFlow.value.copy(unitsToShow = unitsToShow)
_unitsToShow.update { unitsToShow }
}
}
}

View File

@ -30,6 +30,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.BuildConfig
import com.sadellie.unitto.R
import com.sadellie.unitto.data.NavRoutes.ABOUT_SCREEN
@ -51,6 +52,7 @@ fun SettingsScreen(
navControllerAction: (String) -> Unit
) {
val mContext = LocalContext.current
val userPrefs = viewModel.userPrefs.collectAsStateWithLifecycle()
var dialogState: DialogState by rememberSaveable {
mutableStateOf(DialogState.NONE)
}
@ -151,7 +153,7 @@ fun SettingsScreen(
UnittoListItem(
label = stringResource(R.string.send_usage_statistics),
supportText = stringResource(R.string.send_usage_statistics_support),
switchState = viewModel.userPrefs.enableAnalytics
switchState = userPrefs.value.enableAnalytics
) { viewModel.updateEnableAnalytics(it) }
}
}
@ -198,7 +200,7 @@ fun SettingsScreen(
AlertDialogWithList(
title = stringResource(R.string.precision_setting),
listItems = PRECISIONS,
selectedItemIndex = viewModel.userPrefs.digitsPrecision,
selectedItemIndex = userPrefs.value.digitsPrecision,
selectAction = { viewModel.updatePrecision(it) },
dismissAction = { resetDialog() },
supportText = stringResource(R.string.precision_setting_info)
@ -208,7 +210,7 @@ fun SettingsScreen(
AlertDialogWithList(
title = stringResource(R.string.separator_setting),
listItems = SEPARATORS,
selectedItemIndex = viewModel.userPrefs.separator,
selectedItemIndex = userPrefs.value.separator,
selectAction = { viewModel.updateSeparator(it) },
dismissAction = { resetDialog() }
)
@ -217,7 +219,7 @@ fun SettingsScreen(
AlertDialogWithList(
title = stringResource(R.string.output_format_setting),
listItems = OUTPUT_FORMAT,
selectedItemIndex = viewModel.userPrefs.outputFormat,
selectedItemIndex = userPrefs.value.outputFormat,
selectAction = { viewModel.updateOutputFormat(it) },
dismissAction = { resetDialog() },
supportText = stringResource(R.string.output_format_setting_info)

View File

@ -19,9 +19,6 @@
package com.sadellie.unitto.screens.setttings
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.FirebaseHelper
@ -32,7 +29,10 @@ import com.sadellie.unitto.data.units.UnitGroupsRepository
import com.sadellie.unitto.screens.Formatter
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.sadellie.themmo.ThemingMode
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
@ -43,7 +43,9 @@ class SettingsViewModel @Inject constructor(
private val unitGroupsRepository: UnitGroupsRepository,
private val application: Application,
) : ViewModel() {
var userPrefs: UserPreferences by mutableStateOf(UserPreferences())
var userPrefs = userPrefsRepository.userPreferencesFlow
.onEach { Formatter.setSeparator(it.separator) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UserPreferences())
val shownUnitGroups = unitGroupsRepository.shownUnitGroups
val hiddenUnitGroups = unitGroupsRepository.hiddenUnitGroups
@ -164,11 +166,6 @@ class SettingsViewModel @Inject constructor(
unitGroupsRepository.updateShownGroups(
userPrefsRepository.userPreferencesFlow.first().shownUnitGroups
)
userPrefsRepository.userPreferencesFlow.collect {
userPrefs = it
Formatter.setSeparator(it.separator)
}
}
}
}