refactor(settings): emit navigation and haptic actions as one-off screen effects
This commit is contained in:
@@ -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()
|
||||
|
||||
+17
-5
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-3
@@ -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(
|
||||
|
||||
-16
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user