refactor(settings): emit navigation and haptic actions as one-off screen effects

This commit is contained in:
2026-05-30 21:17:37 +03:00
parent a1278f7558
commit dc5b4b3fb0
7 changed files with 57 additions and 59 deletions
@@ -8,6 +8,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flow
@@ -15,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -46,6 +48,12 @@ context(viewModel: ViewModel)
fun <T> Flow<T>.listenValue(action: suspend (T) -> Unit): Job =
listenValue(viewModel.viewModelScope, action)
context(viewModel: ViewModel)
fun <T> MutableSharedFlow<T>.emitOnMain(value: T) {
val flow = this
viewModel.viewModelScope.launch { flow.emit(value) }
}
fun <T> Flow<T>.listenValue(
coroutineScope: CoroutineScope,
action: suspend (T) -> Unit
@@ -29,6 +29,7 @@ import dev.meloda.fast.logger.FastLogger
import dev.meloda.fast.model.database.AccountEntity
import dev.meloda.fast.settings.model.HapticType
import dev.meloda.fast.settings.model.SettingsDialog
import dev.meloda.fast.settings.model.SettingsEffect
import dev.meloda.fast.settings.model.SettingsIntent
import dev.meloda.fast.settings.model.SettingsItem
import dev.meloda.fast.settings.model.SettingsNavigationIntent
@@ -36,11 +37,10 @@ import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.settings.model.TextProvider
import dev.meloda.fast.ui.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class SettingsViewModel(
@@ -56,11 +56,8 @@ class SettingsViewModel(
private val screenState = MutableStateFlow(SettingsScreenState.EMPTY)
val screenStateFlow get() = screenState.asStateFlow()
private val hapticType = MutableStateFlow<HapticType?>(null)
val hapticTypeFlow get() = hapticType.asStateFlow()
private val navigationIntent = MutableStateFlow<SettingsNavigationIntent?>(null)
val navigationIntentFlow get() = navigationIntent.asStateFlow()
private val screenEffect = MutableSharedFlow<SettingsEffect>(extraBufferCapacity = 1)
val screenEffectFlow = screenEffect.asSharedFlow()
private val settings = mutableListOf<SettingsItem<*>>()
private var showDebugCategory: Boolean = userSettings.showDebugCategory.value
@@ -72,11 +69,7 @@ class SettingsViewModel(
fun handleIntent(intent: SettingsIntent) {
when (intent) {
SettingsIntent.BackClick -> {
navigationIntent.setValue { SettingsNavigationIntent.Back }
}
SettingsIntent.ConsumePerformHaptic -> {
hapticType.setValue { null }
screenEffect.tryEmit(SettingsEffect.Navigate(SettingsNavigationIntent.Back))
}
is SettingsIntent.ItemClick -> {
@@ -148,7 +141,10 @@ class SettingsViewModel(
UserConfig.accessToken = oldToken
},
success = { user ->
if (user == null) return@listenValue
if (user == null) {
UserConfig.accessToken = oldToken
return@listenValue
}
UserConfig.currentUserId = user.id
@@ -169,7 +165,9 @@ class SettingsViewModel(
accountsRepository.storeAccounts(listOf(account))
navigationIntent.setValue { SettingsNavigationIntent.Restart }
screenEffect.tryEmit(
SettingsEffect.Navigate(SettingsNavigationIntent.Restart)
)
}
)
}
@@ -204,26 +202,21 @@ class SettingsViewModel(
private fun onLogOutAlertPositiveClick() {
viewModelScope.launch(Dispatchers.IO) {
val tasks = listOf(
async {
accountsRepository.storeAccounts(
listOf(
AccountEntity(
userId = UserConfig.userId,
accessToken = "",
fastToken = UserConfig.fastToken,
trustedHash = UserConfig.trustedHash,
exchangeToken = null
)
)
accountsRepository.storeAccounts(
listOf(
AccountEntity(
userId = UserConfig.userId,
accessToken = "",
fastToken = UserConfig.fastToken,
trustedHash = UserConfig.trustedHash,
exchangeToken = null
)
},
async { UserConfig.clear() }
)
)
tasks.awaitAll()
UserConfig.clear()
navigationIntent.setValue { SettingsNavigationIntent.LogOut }
screenEffect.tryEmit(SettingsEffect.Navigate(SettingsNavigationIntent.LogOut))
}
}
@@ -263,7 +256,7 @@ class SettingsViewModel(
createSettings()
hapticType.update { HapticType.REJECT }
screenEffect.tryEmit(SettingsEffect.PerformHaptic(HapticType.REJECT))
showDebugCategory = false
}
}
@@ -279,7 +272,7 @@ class SettingsViewModel(
createSettings()
hapticType.update { HapticType.LONG_PRESS }
screenEffect.tryEmit(SettingsEffect.PerformHaptic(HapticType.LONG_PRESS))
showDebugCategory = true
}
}
@@ -0,0 +1,6 @@
package dev.meloda.fast.settings.model
sealed interface SettingsEffect {
data class Navigate(val intent: SettingsNavigationIntent) : SettingsEffect
data class PerformHaptic(val type: HapticType) : SettingsEffect
}
@@ -9,8 +9,6 @@ sealed class SettingsIntent {
data class ItemLongClick(val key: String) : SettingsIntent()
data class ItemValueChanged(val key: String, val newValue: Any?) : SettingsIntent()
data object ConsumePerformHaptic : SettingsIntent()
sealed class Dialog : SettingsIntent() {
data object Dismiss : Dialog()
data class ConfirmClick(val bundle: Bundle? = null) : Dialog()
@@ -2,13 +2,18 @@ package dev.meloda.fast.settings.navigation
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.settings.SettingsViewModel
import dev.meloda.fast.settings.model.SettingsEffect
import dev.meloda.fast.settings.model.SettingsNavigationIntent
import dev.meloda.fast.settings.presentation.SettingsRoute
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.Serializable
import org.koin.androidx.compose.koinViewModel
@@ -19,19 +24,26 @@ fun NavGraphBuilder.settingsScreen(
handleNavigationIntent: (SettingsNavigationIntent) -> Unit
) {
composable<Settings> {
val view = LocalView.current
val viewModel: SettingsViewModel = koinViewModel()
val screenState by viewModel.screenStateFlow.collectAsStateWithLifecycle()
val hapticType by viewModel.hapticTypeFlow.collectAsStateWithLifecycle()
val navigationIntent by viewModel.navigationIntentFlow.collectAsStateWithLifecycle()
LaunchedEffect(navigationIntent) {
navigationIntent?.let(handleNavigationIntent)
LaunchedEffect(Unit) {
viewModel.screenEffectFlow.onEach { effect ->
when (effect) {
is SettingsEffect.Navigate -> handleNavigationIntent(effect.intent)
is SettingsEffect.PerformHaptic -> {
if (AppSettings.General.enableHaptic) {
view.performHapticFeedback(effect.type.getHaptic())
}
}
}
}.collect()
}
SettingsRoute(
handleIntent = viewModel::handleIntent,
screenState = screenState,
hapticType = hapticType
)
}
}
@@ -1,7 +1,6 @@
package dev.meloda.fast.settings.presentation
import androidx.compose.runtime.Composable
import dev.meloda.fast.settings.model.HapticType
import dev.meloda.fast.settings.model.SettingsIntent
import dev.meloda.fast.settings.model.SettingsScreenState
@@ -9,12 +8,10 @@ import dev.meloda.fast.settings.model.SettingsScreenState
fun SettingsRoute(
handleIntent: (SettingsIntent) -> Unit,
screenState: SettingsScreenState,
hapticType: HapticType?
) {
SettingsScreen(
handleIntent = handleIntent,
screenState = screenState,
hapticType = hapticType
)
HandleDialogs(
@@ -21,12 +21,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -36,8 +34,6 @@ import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.settings.model.HapticType
import dev.meloda.fast.settings.model.SettingsIntent
import dev.meloda.fast.settings.model.SettingsScreenState
import dev.meloda.fast.settings.model.UiItem
@@ -58,10 +54,7 @@ import dev.meloda.fast.ui.theme.LocalThemeConfig
fun SettingsScreen(
handleIntent: (SettingsIntent) -> Unit,
screenState: SettingsScreenState = SettingsScreenState.EMPTY,
hapticType: HapticType?
) {
val view = LocalView.current
val onSettingsItemClicked by rememberUpdatedState { key: String ->
handleIntent(SettingsIntent.ItemClick(key))
}
@@ -74,15 +67,6 @@ fun SettingsScreen(
handleIntent(SettingsIntent.ItemValueChanged(key, newValue))
}
LaunchedEffect(hapticType) {
if (hapticType != null) {
if (AppSettings.General.enableHaptic) {
view.performHapticFeedback(hapticType.getHaptic())
}
handleIntent(SettingsIntent.ConsumePerformHaptic)
}
}
val themeConfig = LocalThemeConfig.current
val hazeState = remember { HazeState(true) }