refactor(settings): move state variables to single state class

#117
This commit is contained in:
Nishant Mishra
2025-11-09 11:47:18 +05:30
parent 538c984d40
commit e1fa6c28b9
9 changed files with 59 additions and 91 deletions

View File

@@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.asStateFlow
*/ */
class FossBillingManager : BillingManager { class FossBillingManager : BillingManager {
override val isPlus = MutableStateFlow(true).asStateFlow() override val isPlus = MutableStateFlow(true).asStateFlow()
override val isLoaded = MutableStateFlow(true).asStateFlow()
} }
object BillingManagerProvider { object BillingManagerProvider {

View File

@@ -61,18 +61,6 @@ class MainActivity : ComponentActivity() {
val seed = settingsState.colorScheme.toColor() val seed = settingsState.colorScheme.toColor()
val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle() val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle()
val isPurchaseStateLoaded by settingsViewModel.isPurchaseStateLoaded.collectAsStateWithLifecycle()
val isSettingsLoaded by settingsViewModel.isSettingsLoaded.collectAsStateWithLifecycle()
LaunchedEffect(isPurchaseStateLoaded, isPlus, isSettingsLoaded) {
if (isPurchaseStateLoaded && isSettingsLoaded) {
if (!isPlus) {
settingsViewModel.resetPaywalledSettings()
} else {
settingsViewModel.reloadSettings()
}
}
}
TomatoTheme( TomatoTheme(
darkTheme = darkTheme, darkTheme = darkTheme,

View File

@@ -21,5 +21,4 @@ import kotlinx.coroutines.flow.StateFlow
interface BillingManager { interface BillingManager {
val isPlus: StateFlow<Boolean> val isPlus: StateFlow<Boolean>
val isLoaded: StateFlow<Boolean>
} }

View File

@@ -101,10 +101,6 @@ fun SettingsScreenRoot(
val longBreakTimeInputFieldState = viewModel.longBreakTimeTextFieldState val longBreakTimeInputFieldState = viewModel.longBreakTimeTextFieldState
val isPlus by viewModel.isPlus.collectAsStateWithLifecycle() val isPlus by viewModel.isPlus.collectAsStateWithLifecycle()
val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound)
val settingsState by viewModel.settingsState.collectAsStateWithLifecycle() val settingsState by viewModel.settingsState.collectAsStateWithLifecycle()
@@ -125,10 +121,6 @@ fun SettingsScreenRoot(
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled,
alarmSound = alarmSound,
onAction = viewModel::onAction, onAction = viewModel::onAction,
setShowPaywall = setShowPaywall, setShowPaywall = setShowPaywall,
modifier = modifier modifier = modifier
@@ -146,10 +138,6 @@ private fun SettingsScreen(
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
alarmEnabled: Boolean,
vibrateEnabled: Boolean,
dndEnabled: Boolean,
alarmSound: String,
onAction: (SettingsAction) -> Unit, onAction: (SettingsAction) -> Unit,
setShowPaywall: (Boolean) -> Unit, setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -288,9 +276,6 @@ private fun SettingsScreen(
entry<Screen.Settings.Alarm> { entry<Screen.Settings.Alarm> {
AlarmSettings( AlarmSettings(
settingsState = settingsState, settingsState = settingsState,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
alarmSound = alarmSound,
onAction = onAction, onAction = onAction,
onBack = backStack::removeLastOrNull onBack = backStack::removeLastOrNull
) )
@@ -307,8 +292,7 @@ private fun SettingsScreen(
entry<Screen.Settings.Timer> { entry<Screen.Settings.Timer> {
TimerSettings( TimerSettings(
isPlus = isPlus, isPlus = isPlus,
aodEnabled = settingsState.aodEnabled, settingsState = settingsState,
dndEnabled = dndEnabled,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,

View File

@@ -79,9 +79,6 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable @Composable
fun AlarmSettings( fun AlarmSettings(
settingsState: SettingsState, settingsState: SettingsState,
alarmEnabled: Boolean,
vibrateEnabled: Boolean,
alarmSound: String,
onAction: (SettingsAction) -> Unit, onAction: (SettingsAction) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -91,10 +88,11 @@ fun AlarmSettings(
var alarmName by remember { mutableStateOf("...") } var alarmName by remember { mutableStateOf("...") }
LaunchedEffect(alarmSound) { LaunchedEffect(settingsState.alarmSound) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
alarmName = alarmName =
RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: "" RingtoneManager.getRingtone(context, settingsState.alarmSound.toUri())
?.getTitle(context) ?: ""
} }
} }
@@ -117,30 +115,30 @@ fun AlarmSettings(
} }
@SuppressLint("LocalContextGetResourceValueCall") @SuppressLint("LocalContextGetResourceValueCall")
val ringtonePickerIntent = remember(alarmSound) { val ringtonePickerIntent = remember(settingsState.alarmSound) {
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound)) putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound))
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri()) putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, settingsState.alarmSound.toUri())
} }
} }
val switchItems = remember( val switchItems = remember(
settingsState.blackTheme, settingsState.blackTheme,
settingsState.aodEnabled, settingsState.aodEnabled,
alarmEnabled, settingsState.alarmEnabled,
vibrateEnabled settingsState.vibrateEnabled
) { ) {
listOf( listOf(
SettingsSwitchItem( SettingsSwitchItem(
checked = alarmEnabled, checked = settingsState.alarmEnabled,
icon = R.drawable.alarm_on, icon = R.drawable.alarm_on,
label = R.string.sound, label = R.string.sound,
description = R.string.alarm_desc, description = R.string.alarm_desc,
onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) } onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) }
), ),
SettingsSwitchItem( SettingsSwitchItem(
checked = vibrateEnabled, checked = settingsState.vibrateEnabled,
icon = R.drawable.mobile_vibrate, icon = R.drawable.mobile_vibrate,
label = R.string.vibrate, label = R.string.vibrate,
description = R.string.vibrate_desc, description = R.string.vibrate_desc,
@@ -245,9 +243,7 @@ fun AlarmSettingsPreview() {
val settingsState = SettingsState() val settingsState = SettingsState()
AlarmSettings( AlarmSettings(
settingsState = settingsState, settingsState = settingsState,
alarmEnabled = true,
vibrateEnabled = false,
alarmSound = "",
onAction = {}, onAction = {},
onBack = {}) onBack = {}
)
} }

View File

@@ -41,6 +41,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.FilledTonalIconToggleButton
@@ -57,6 +58,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberSliderState
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
@@ -78,6 +80,7 @@ import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -92,14 +95,13 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable @Composable
fun TimerSettings( fun TimerSettings(
isPlus: Boolean, isPlus: Boolean,
aodEnabled: Boolean, settingsState: SettingsState,
dndEnabled: Boolean,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
setShowPaywall: (Boolean) -> Unit,
onAction: (SettingsAction) -> Unit, onAction: (SettingsAction) -> Unit,
setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -116,7 +118,7 @@ fun TimerSettings(
val switchItems = listOf( val switchItems = listOf(
SettingsSwitchItem( SettingsSwitchItem(
checked = dndEnabled, checked = settingsState.dndEnabled,
icon = R.drawable.dnd, icon = R.drawable.dnd,
label = R.string.dnd, label = R.string.dnd,
description = R.string.dnd_desc, description = R.string.dnd_desc,
@@ -133,7 +135,7 @@ fun TimerSettings(
} }
), ),
SettingsSwitchItem( SettingsSwitchItem(
checked = aodEnabled, checked = settingsState.aodEnabled,
icon = R.drawable.aod, icon = R.drawable.aod,
label = R.string.always_on_display, label = R.string.always_on_display,
description = R.string.always_on_display_desc, description = R.string.always_on_display_desc,
@@ -393,18 +395,17 @@ fun TimerSettings(
@Preview @Preview
@Composable @Composable
private fun TimerSettingsPreview() { private fun TimerSettingsPreview() {
val focusTimeInputFieldState = TextFieldState("25") val focusTimeInputFieldState = rememberTextFieldState("25")
val shortBreakTimeInputFieldState = TextFieldState("5") val shortBreakTimeInputFieldState = rememberTextFieldState("5")
val longBreakTimeInputFieldState = TextFieldState("15") val longBreakTimeInputFieldState = rememberTextFieldState("15")
val sessionsSliderState = SliderState( val sessionsSliderState = rememberSliderState(
value = 4f, value = 4f,
valueRange = 1f..8f, valueRange = 1f..8f,
steps = 6 steps = 6
) )
TimerSettings( TimerSettings(
isPlus = false, isPlus = false,
aodEnabled = true, settingsState = remember { SettingsState() },
dndEnabled = false,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,

View File

@@ -23,7 +23,11 @@ import androidx.compose.ui.graphics.Color
@Immutable @Immutable
data class SettingsState( data class SettingsState(
val theme: String = "auto", val theme: String = "auto",
val alarmSound: String = "",
val colorScheme: String = Color.White.toString(), val colorScheme: String = Color.White.toString(),
val blackTheme: Boolean = false, val blackTheme: Boolean = false,
val aodEnabled: Boolean = false val aodEnabled: Boolean = false,
val alarmEnabled: Boolean = true,
val vibrateEnabled: Boolean = true,
val dndEnabled: Boolean = false
) )

View File

@@ -18,6 +18,7 @@
package org.nsh07.pomodoro.ui.settingsScreen.viewModel package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import android.net.Uri import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SliderState import androidx.compose.material3.SliderState
@@ -36,7 +37,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
@@ -54,10 +54,6 @@ class SettingsViewModel(
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main) val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
val isPlus = billingManager.isPlus val isPlus = billingManager.isPlus
val isPurchaseStateLoaded = billingManager.isLoaded
private val _isSettingsLoaded = MutableStateFlow(false)
val isSettingsLoaded = _isSettingsLoaded.asStateFlow()
private val _settingsState = MutableStateFlow(SettingsState()) private val _settingsState = MutableStateFlow(SettingsState())
val settingsState = _settingsState.asStateFlow() val settingsState = _settingsState.asStateFlow()
@@ -81,25 +77,13 @@ class SettingsViewModel(
) )
} }
val currentAlarmSound = timerRepository.alarmSoundUri.toString()
private var focusFlowCollectionJob: Job? = null private var focusFlowCollectionJob: Job? = null
private var shortBreakFlowCollectionJob: Job? = null private var shortBreakFlowCollectionJob: Job? = null
private var longBreakFlowCollectionJob: Job? = null private var longBreakFlowCollectionJob: Job? = null
val alarmSound =
preferenceRepository.getStringPreferenceFlow("alarm_sound").distinctUntilChanged()
val alarmEnabled =
preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
val vibrateEnabled =
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
val dndEnabled =
preferenceRepository.getBooleanPreferenceFlow("dnd_enabled").distinctUntilChanged()
init { init {
viewModelScope.launch { viewModelScope.launch {
reloadSettings() reloadSettings()
_isSettingsLoaded.value = true
} }
} }
@@ -176,6 +160,9 @@ class SettingsViewModel(
private fun saveAlarmEnabled(enabled: Boolean) { private fun saveAlarmEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmEnabled = enabled timerRepository.alarmEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(alarmEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("alarm_enabled", enabled) preferenceRepository.saveBooleanPreference("alarm_enabled", enabled)
} }
} }
@@ -183,6 +170,9 @@ class SettingsViewModel(
private fun saveVibrateEnabled(enabled: Boolean) { private fun saveVibrateEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.vibrateEnabled = enabled timerRepository.vibrateEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(vibrateEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled) preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled)
} }
} }
@@ -190,6 +180,9 @@ class SettingsViewModel(
private fun saveDndEnabled(enabled: Boolean) { private fun saveDndEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.dndEnabled = enabled timerRepository.dndEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(dndEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("dnd_enabled", enabled) preferenceRepository.saveBooleanPreference("dnd_enabled", enabled)
} }
} }
@@ -197,6 +190,9 @@ class SettingsViewModel(
private fun saveAlarmSound(uri: Uri?) { private fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmSoundUri = uri timerRepository.alarmSoundUri = uri
_settingsState.update { currentState ->
currentState.copy(alarmSound = uri.toString())
}
preferenceRepository.saveStringPreference("alarm_sound", uri.toString()) preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
} }
} }
@@ -237,16 +233,6 @@ class SettingsViewModel(
} }
} }
fun resetPaywalledSettings() {
_settingsState.update { currentState ->
currentState.copy(
aodEnabled = false,
blackTheme = false,
colorScheme = Color.White.toString()
)
}
}
suspend fun reloadSettings() { suspend fun reloadSettings() {
val theme = preferenceRepository.getStringPreference("theme") val theme = preferenceRepository.getStringPreference("theme")
?: preferenceRepository.saveStringPreference("theme", "auto") ?: preferenceRepository.saveStringPreference("theme", "auto")
@@ -256,13 +242,29 @@ class SettingsViewModel(
?: preferenceRepository.saveBooleanPreference("black_theme", false) ?: preferenceRepository.saveBooleanPreference("black_theme", false)
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled") val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false) ?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
val alarmSound = preferenceRepository.getStringPreference("alarm_sound")
?: preferenceRepository.saveStringPreference(
"alarm_sound",
(Settings.System.DEFAULT_ALARM_ALERT_URI
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
)
val alarmEnabled = preferenceRepository.getBooleanPreference("alarm_enabled")
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
val vibrateEnabled = preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled")
?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
_settingsState.update { currentState -> _settingsState.update { currentState ->
currentState.copy( currentState.copy(
theme = theme, theme = theme,
colorScheme = colorScheme, colorScheme = colorScheme,
alarmSound = alarmSound,
blackTheme = blackTheme, blackTheme = blackTheme,
aodEnabled = aodEnabled aodEnabled = aodEnabled,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled
) )
} }
} }

View File

@@ -33,9 +33,6 @@ class PlayBillingManager : BillingManager {
private val _isPlus = MutableStateFlow(false) private val _isPlus = MutableStateFlow(false)
override val isPlus = _isPlus.asStateFlow() override val isPlus = _isPlus.asStateFlow()
private val _isLoaded = MutableStateFlow(false)
override val isLoaded = _isLoaded.asStateFlow()
private val purchases by lazy { Purchases.sharedInstance } private val purchases by lazy { Purchases.sharedInstance }
init { init {
@@ -48,11 +45,9 @@ class PlayBillingManager : BillingManager {
purchases.getCustomerInfoWith( purchases.getCustomerInfoWith(
onSuccess = { customerInfo -> onSuccess = { customerInfo ->
_isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true _isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
_isLoaded.value = true
}, },
onError = { error -> onError = { error ->
Log.e("GooglePlayPaywallManager", "Error fetching customer info: $error") Log.e("GooglePlayPaywallManager", "Error fetching customer info: $error")
_isLoaded.value = true
} }
) )
} }