add currency settings

This commit is contained in:
MM20
2026-06-29 19:13:30 +02:00
parent fd25df0121
commit 8b50c8349d
52 changed files with 661 additions and 132 deletions

View File

@@ -3,9 +3,6 @@ package de.mm20.launcher2.ui.component.preferences
import androidx.activity.compose.LocalActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
@@ -22,11 +19,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -37,13 +31,16 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.dragndrop.LazyDragAndDropColumn
import de.mm20.launcher2.ui.component.dragndrop.LazyDragAndDropListState
import de.mm20.launcher2.ui.component.dragndrop.rememberLazyDragAndDropListState
import de.mm20.launcher2.ui.locals.LocalBackStack
@Composable
@Deprecated("Use the overload with title as a composable function instead")
fun PreferenceScreen(
title: String,
floatingActionButton: @Composable () -> Unit = {},
topBarActions: @Composable RowScope.() -> Unit = {},
helpUrl: String? = null,
lazyColumnState: LazyListState = rememberLazyListState(),
@@ -59,7 +56,6 @@ fun PreferenceScreen(
maxLines = 1
)
},
floatingActionButton = floatingActionButton,
topBarActions = topBarActions,
helpUrl = helpUrl,
lazyColumnState = lazyColumnState,
@@ -71,75 +67,21 @@ fun PreferenceScreen(
@Composable
fun PreferenceScreen(
title: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
topBarActions: @Composable RowScope.() -> Unit = {},
helpUrl: String? = null,
lazyColumnState: LazyListState = rememberLazyListState(),
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(12.dp),
content: LazyListScope.() -> Unit,
) {
val backStack = LocalBackStack.current
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme
var fabVisible by remember { mutableStateOf(true) }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val activity = LocalActivity.current
Scaffold(
floatingActionButton = {
AnimatedVisibility(
fabVisible,
enter = scaleIn(),
exit = scaleOut(),
) {
floatingActionButton()
}
},
topBar = {
CenterAlignedTopAppBar(
PreferenceScreenTopBar(
title = title,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
),
navigationIcon = {
IconButton(onClick = {
if (backStack.size <= 1) {
activity?.onBackPressed()
} else {
backStack.removeLastOrNull()
}
}) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = "Back"
)
}
},
actions = {
if (helpUrl != null) {
IconButton(onClick = {
CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(colorScheme.primaryContainer.toArgb())
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
.build()
)
.build().launchUrl(context, helpUrl.toUri())
}) {
Icon(
painter = painterResource(R.drawable.help_24px),
contentDescription = stringResource(R.string.help)
)
}
}
topBarActions()
},
actions = topBarActions,
helpUrl = helpUrl,
scrollBehavior = scrollBehavior,
)
},
@@ -160,5 +102,101 @@ fun PreferenceScreen(
)
)
}
}
@Composable
fun DragAndDropPreferenceScreen(
title: @Composable () -> Unit = {},
topBarActions: @Composable RowScope.() -> Unit = {},
helpUrl: String? = null,
lazyColumnState: LazyDragAndDropListState,
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(1.dp),
content: LazyListScope.() -> Unit,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
topBar = {
PreferenceScreenTopBar(
title = title,
actions = topBarActions,
helpUrl = helpUrl,
scrollBehavior = scrollBehavior,
)
},
containerColor = MaterialTheme.colorScheme.surfaceContainer
) {
LazyDragAndDropColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
state = lazyColumnState,
content = content,
verticalArrangement = verticalArrangement,
contentPadding = PaddingValues(
top = it.calculateTopPadding() + 12.dp,
bottom = it.calculateBottomPadding() + 12.dp,
start = 12.dp,
end = 12.dp
),
bidirectionalDrag = false,
)
}
}
@Composable
private fun PreferenceScreenTopBar(
title: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
helpUrl: String? = null,
scrollBehavior: TopAppBarScrollBehavior,
) {
val backStack = LocalBackStack.current
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme
val activity = LocalActivity.current
CenterAlignedTopAppBar(
title = title,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
),
navigationIcon = {
IconButton(onClick = {
if (backStack.size <= 1) {
activity?.onBackPressed()
} else {
backStack.removeLastOrNull()
}
}) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = "Back"
)
}
},
actions = {
if (helpUrl != null) {
IconButton(onClick = {
CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(colorScheme.primaryContainer.toArgb())
.setSecondaryToolbarColor(colorScheme.secondaryContainer.toArgb())
.build()
)
.build().launchUrl(context, helpUrl.toUri())
}) {
Icon(
painter = painterResource(R.drawable.help_24px),
contentDescription = stringResource(R.string.help)
)
}
}
actions()
},
scrollBehavior = scrollBehavior,
)
}

View File

@@ -100,6 +100,8 @@ import de.mm20.launcher2.ui.settings.license.LicenseRoute
import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.locale.CalendarSettingsRoute
import de.mm20.launcher2.ui.settings.locale.CalendarSettingsScreen
import de.mm20.launcher2.ui.settings.locale.CurrencySettingsRoute
import de.mm20.launcher2.ui.settings.locale.CurrencySettingsScreen
import de.mm20.launcher2.ui.settings.locale.LocaleSettingsRoute
import de.mm20.launcher2.ui.settings.locale.LocaleSettingsScreen
import de.mm20.launcher2.ui.settings.locations.LocationsSettingsRoute
@@ -304,6 +306,9 @@ class SettingsActivity : BaseActivity() {
entry<CalendarSettingsRoute> {
CalendarSettingsScreen()
}
entry<CurrencySettingsRoute> {
CurrencySettingsScreen()
}
entry<BackupSettingsRoute> {
BackupSettingsScreen()
}

View File

@@ -93,7 +93,7 @@ fun CorePaletteColorPreference(
) {
SwitchPreference(
icon = R.drawable.rule_settings_24px,
title = stringResource(R.string.theme_color_scheme_system_default),
title = stringResource(R.string.preference_use_system_default),
value = currentValue == null,
onValueChanged = {
currentValue = if (it) null else defaultValue

View File

@@ -0,0 +1,366 @@
package de.mm20.launcher2.ui.settings.locale
import android.icu.util.Currency
import android.icu.util.ULocale
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuGroup
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.DropdownMenuPopup
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalLocale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.navigation3.runtime.NavKey
import de.mm20.launcher2.preferences.ui.LocaleSettings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.DismissableBottomSheet
import de.mm20.launcher2.ui.component.dragndrop.DraggableItem
import de.mm20.launcher2.ui.component.dragndrop.rememberLazyDragAndDropListState
import de.mm20.launcher2.ui.component.preferences.DragAndDropPreferenceScreen
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.ktx.animateShapeAsState
import kotlinx.serialization.Serializable
import org.koin.compose.koinInject
import java.util.Date
@Serializable
data object CurrencySettingsRoute : NavKey
@Composable
fun CurrencySettingsScreen() {
val localeSettings = koinInject<LocaleSettings>()
val currencies by localeSettings.currencies.collectAsState(null)
val dragAndDropListState = rememberLazyDragAndDropListState(
onDragStart = {
it.key.toString().startsWith("currency_")
},
onItemMove = { from, to ->
val currencies = currencies?.toMutableList() ?: return@rememberLazyDragAndDropListState
val fromIndex = from.index - 2
val toIndex = to.index - 2
if (fromIndex > currencies.lastIndex || fromIndex < 0) return@rememberLazyDragAndDropListState
if (toIndex > currencies.lastIndex || toIndex < 0) return@rememberLazyDragAndDropListState
val item = currencies.removeAt(fromIndex)
currencies.add(toIndex, item)
localeSettings.setCurrencies(currencies)
}
)
val locale = LocalLocale.current
val defaultCurrency = remember(locale) {
Currency.getInstance(locale.platformLocale)
}
DragAndDropPreferenceScreen(
title = {
Text(stringResource(R.string.preference_currencies))
},
lazyColumnState = dragAndDropListState,
) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp)
) {
PreferenceCategory {
SwitchPreference(
title = stringResource(R.string.preference_use_system_default),
value = currencies?.isEmpty() == true,
onValueChanged = {
if (it) {
localeSettings.setCurrencies(emptyList())
} else {
localeSettings.setCurrencies(listOf(defaultCurrency.currencyCode))
}
},
)
}
}
}
if (currencies != null) {
if (currencies!!.isEmpty()) {
item {
PreferenceCategory {
Preference(
title = defaultCurrency.displayName,
icon = R.drawable.euro_24px,
enabled = false
)
}
}
} else {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Icon(
painterResource(R.drawable.info_20px), null,
modifier = Modifier
.padding(end = 12.dp)
.size(16.dp),
tint = MaterialTheme.colorScheme.secondary
)
Text(
modifier = Modifier,
text = stringResource(R.string.hint_drag_and_drop_reorder),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.secondary
)
}
}
itemsIndexed(
items = currencies!!,
key = { _, it -> "currency_$it" }
) { index, currency ->
DraggableItem(
state = dragAndDropListState,
key = "currency_$currency"
) {
val shape = getShape(index, currencies!!.size)
val draggedShape = MaterialTheme.shapes.medium
val elevation by animateDpAsState(if (it) 4.dp else 0.dp)
val currencyName = try {
Currency.getInstance(currency).displayName
} catch (e: IllegalArgumentException) {
currency
}
Surface(
shadowElevation = elevation,
tonalElevation = elevation,
modifier = Modifier
.zIndex(if (it) 1f else 0f),
shape = animateShapeAsState(
if (it) draggedShape else shape
).value
) {
Preference(
title = currencyName,
icon = getCurrencySymbol(currency),
iconPadding = true,
enabled = true,
controls = {
var showMenu by remember { mutableStateOf(false) }
IconButton(
onClick = { showMenu = true }
) {
Icon(
painterResource(R.drawable.more_vert_24px),
stringResource(R.string.action_more_actions),
)
}
DropdownMenuPopup(
expanded = showMenu,
onDismissRequest = { showMenu = false },
) {
DropdownMenuGroup(
shapes = MenuDefaults.groupShapes()
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_remove)) },
leadingIcon = {
Icon(painterResource(R.drawable.delete_24px), null)
},
onClick = {
val newCurrencies = currencies!!.toMutableList()
newCurrencies.removeAt(index)
localeSettings.setCurrencies(newCurrencies)
showMenu = false
}
)
}
}
}
)
}
}
}
item {
var showModal by remember { mutableStateOf(false) }
FilledTonalButton(
modifier = Modifier
.padding(top = 10.dp)
.navigationBarsPadding(),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
onClick = {
showModal = true
}) {
Icon(
painterResource(R.drawable.add_20px),
null,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize)
)
Text(stringResource(R.string.preference_add_currency))
}
CurrencyPickerSheet(
selectedCurrencies = currencies!!,
onCurrencySelected = { currency ->
val newCurrencies = currencies!!.toMutableList()
newCurrencies.add(currency)
localeSettings.setCurrencies(newCurrencies)
showModal = false
},
expanded = showModal,
onDismissRequest = { showModal = false }
)
}
}
}
}
}
private fun getCurrencySymbol(currencyCode: String): Int? {
return when (currencyCode) {
"AUD", "BBD", "BMD", "BND", "BSD", "BZD", "CAD", "FJD", "GYD", "HKD", "JMD", "KID", "KYD",
"LRD", "NAD", "NZD", "SBD", "SGD", "SRD", "TTD", "TVD", "TWD", "USD", "XCD", "ARS", "CLP",
"COP", "CUP", "DOP", "MXN", "UYU" -> R.drawable.attach_money_24px
"EUR" -> R.drawable.euro_24px
"EGP", "FKP", "GBP", "GIP", "SHP", "SDG", "SYP" -> R.drawable.currency_pound_24px
"JPY" -> R.drawable.currency_yen_24px
"CNY" -> R.drawable.currency_yuan_24px
"INR" -> R.drawable.currency_rupee_24px
"RUB" -> R.drawable.currency_ruble_24px
else -> R.drawable.toll_24px
}
}
@Composable
private fun getShape(index: Int, total: Int): Shape {
if (total == 1) {
return MaterialTheme.shapes.medium
}
if (total > 1 && index > 0 && index < total - 1) {
return MaterialTheme.shapes.extraSmall
}
val xs = MaterialTheme.shapes.extraSmall
val md = MaterialTheme.shapes.medium
if (index == 0) {
return xs.copy(
topStart = md.topStart,
topEnd = md.topEnd
)
} else {
return xs.copy(
bottomStart = md.bottomStart,
bottomEnd = md.bottomEnd
)
}
}
@Composable
private fun CurrencyPickerSheet(
selectedCurrencies: List<String>,
onCurrencySelected: (String) -> Unit,
expanded: Boolean,
onDismissRequest: () -> Unit,
) {
val availableCurrencies = remember {
val locales = ULocale.getAvailableLocales()
val currencyCodes = mutableSetOf<String>()
for (locale in locales) {
val currencies = Currency.getAvailableCurrencyCodes(locale, Date()) ?: continue
currencyCodes.addAll(currencies)
}
val currencies = mutableListOf<Currency>()
for (currencyCode in currencyCodes) {
try {
currencies += Currency.getInstance(currencyCode)
} catch (e: IllegalArgumentException) {
// Ignore invalid currency codes
}
}
currencies.sortBy { it.displayName }
currencies
}
DismissableBottomSheet(
expanded = expanded,
onDismissRequest = onDismissRequest
) {
LazyColumn(
contentPadding = PaddingValues(
top = 12.dp,
bottom = WindowInsets.navigationBars.asPaddingValues()
.calculateBottomPadding() + 12.dp,
start = 12.dp,
end = 12.dp
),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
itemsIndexed(availableCurrencies) { i, currency ->
Box(
modifier = Modifier.clip(
shape = getShape(i, availableCurrencies.size)
)
) {
Preference(
title = currency.displayName,
onClick = {
onCurrencySelected(currency.currencyCode)
}
)
}
}
}
}
}

View File

@@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.settings.locale
import android.content.Intent
import android.icu.text.ListFormatter
import android.icu.text.Transliterator
import android.icu.util.Currency
import android.icu.util.ULocale
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
@@ -43,6 +44,7 @@ fun LocaleSettingsScreen() {
val measurementSystem by viewModel.measurementSystem.collectAsStateWithLifecycle(null)
val transliterator by viewModel.transliterator.collectAsStateWithLifecycle(null)
val calendars by viewModel.calendars.collectAsStateWithLifecycle(emptyList())
val currencies by viewModel.currencies.collectAsStateWithLifecycle(null)
// The language that has been selected by the user, or null to use the system language
val selectedLocale = remember {
@@ -234,6 +236,28 @@ fun LocaleSettingsScreen() {
)
}
)
Preference(
title = stringResource(R.string.preference_currencies),
summary = currencies?.let {
if (it.isEmpty()) {
return@let stringResource(R.string.preference_value_system_default)
}
val names = it.map {
try {
Currency.getInstance(it).displayName
} catch (e: IllegalArgumentException) {
it
}
}
ListFormatter.getInstance().format(names)
},
icon = R.drawable.toll_24px,
onClick = {
backstack += CurrencySettingsRoute
},
)
}
}
}

View File

@@ -30,4 +30,6 @@ class LocaleSettingsScreenVM : ViewModel(), KoinComponent {
val calendars = combine(localeSettings.primaryCalendar, localeSettings.secondaryCalendar) {
it.toImmutableList()
}
val currencies = localeSettings.currencies
}

View File

@@ -121,27 +121,6 @@ fun LogScreen() {
}
},
verticalArrangement = Arrangement.spacedBy(2.dp),
floatingActionButton = {
AnimatedVisibility(
listState.canScrollForward,
enter = scaleIn(),
exit = scaleOut(),
) {
SmallFloatingActionButton(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
onClick = {
scope.launch {
listState.animateScrollToItem(lines.lastIndex)
}
}) {
Icon(
painterResource(R.drawable.arrow_downward_24px),
null,
)
}
}
},
lazyColumnState = listState,
) {
itemsIndexed(lines) { i, it ->

View File

@@ -163,13 +163,15 @@ fun SearchActionsSettingsScreen() {
) {
Row(
modifier = Modifier
.padding(start = 28.dp, top = 16.dp, end = 16.dp, bottom = 16.dp),
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Icon(
painterResource(R.drawable.info_20px), null,
modifier = Modifier
.padding(end = 24.dp)
.padding(end = 12.dp)
.size(16.dp),
tint = MaterialTheme.colorScheme.secondary
)

View File

@@ -12,6 +12,7 @@ import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalBackStack
import de.mm20.launcher2.ui.settings.locale.CurrencySettingsRoute
import kotlinx.serialization.Serializable
@Serializable
@@ -58,6 +59,13 @@ fun UnitConverterSettingsScreen() {
backStack.add(UnitConverterHelpSettingsRoute)
}
)
Preference(
title = stringResource(R.string.preference_currency_settings),
icon = R.drawable.open_in_new_24px,
onClick = {
backStack.add(CurrencySettingsRoute)
}
)
}
}

View File

@@ -1,20 +1,13 @@
package de.mm20.launcher2.ui.settings.unitconverter
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.search.UnitConverterSettings
import de.mm20.launcher2.unitconverter.UnitConverterRepository
import de.mm20.launcher2.unitconverter.converters.Converter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@@ -29,13 +22,13 @@ class UnitConverterSettingsScreenVM: ViewModel(), KoinComponent {
settings.setEnabled(unitConverter)
}
val currencyConverter = settings.currencies
val currencyConverter = settings.currenciesEnabled
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun setCurrencyConverter(currencyConverter: Boolean) {
settings.setCurrencies(currencyConverter)
settings.setCurrenciesEnabled(currencyConverter)
}
val availableConverters = settings.currencies.map {
val availableConverters = settings.currenciesEnabled.map {
repository.getAvailableConverters(includeCurrencies = it)
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(100), 1)