fix: dataStore逻辑

This commit is contained in:
HapeLee
2026-06-08 13:51:11 +08:00
parent b0cb83fcce
commit 9189b3dcfc
8 changed files with 83 additions and 150 deletions
@@ -1007,7 +1007,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
var pureBlack
get() = appCtx.getPrefBoolean(PreferKey.pureBlack, false)
set(value) {
appCtx.getPrefBoolean(PreferKey.pureBlack, value)
appCtx.putPrefBoolean(PreferKey.pureBlack, value)
}
val hasLightBg: Boolean
@@ -19,7 +19,6 @@ import com.google.android.material.card.MaterialCardView
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.utils.getPrefString
@@ -90,7 +89,7 @@ class ThemeCardPreference(context: Context, attrs: AttributeSet) : Preference(co
context.toastOnUi(R.string.transparent_theme_alarm)
return@setOnClickListener
} else {
AppConfig.containerOpacity = 0
ThemeConfig.containerOpacity = 0
}
}
val oldValue = currentValue
@@ -2587,7 +2587,6 @@ class ReadBookViewModel(
else -> "0" // dark → follow system
}
ThemeConfig.themeMode = nextMode
AppConfig.themeMode = nextMode
_uiState.update { it.copy(styleConfig = buildStyleConfig()) }
_effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(
setOf(
@@ -1,17 +1,10 @@
package io.legado.app.ui.config
import android.content.SharedPreferences
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import io.legado.app.utils.defaultSharedPreferences
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefFloat
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefLong
import io.legado.app.utils.getPrefString
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.putPrefFloat
import io.legado.app.utils.putPrefInt
@@ -24,8 +17,10 @@ import io.legado.app.data.repository.dataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import splitties.init.appCtx
@@ -49,39 +44,41 @@ fun <T> prefDelegate(
sync: Boolean = false,
onValueChange: ((T) -> Unit)? = null
): PrefDelegate<T> {
return object : PrefDelegate<T>, SharedPreferences.OnSharedPreferenceChangeListener, DefaultLifecycleObserver {
private var _value: MutableState<T> = mutableStateOf(readInitialValue())
return object : PrefDelegate<T>, DefaultLifecycleObserver {
private var _value: MutableState<T> = mutableStateOf(defaultValue)
override val state: State<T> get() = _value
private val scope = CoroutineScope(Dispatchers.IO)
private var dsObserverJob: Job? = null
init {
if (lifecycleOwner != null) {
lifecycleOwner.lifecycle.addObserver(this)
} else {
appCtx.defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this)
// 从 DataStore 读取初始值(DS 为唯一读取源)
scope.launch {
val dsValue = readFromDs()
if (dsValue != null) {
_value.value = dsValue
}
}
// 观察 DataStore,以 DS 为读取权威源
dsObserverJob = CoroutineScope(Dispatchers.IO).launch {
// 观察 DataStore 变化,用于跨实例同步
dsObserverJob = scope.launch {
appCtx.dataStore.data
.catch { if (it is IOException) emit(emptyPreferences()) else throw it }
.map { prefs ->
// 安全读取 String 值,用于类型回退(类型不匹配时返回 null)
val strVal = runCatching { prefs[stringPreferencesKey(key)] }.getOrNull()
@Suppress("UNCHECKED_CAST")
when {
defaultValue is String || defaultValue == null ->
strVal as T?
defaultValue is Int ->
(prefs[intPreferencesKey(key)]
(runCatching { prefs[intPreferencesKey(key)] }.getOrNull()
?: strVal?.toIntOrNull()) as T?
defaultValue is Boolean ->
(prefs[booleanPreferencesKey(key)]
(runCatching { prefs[booleanPreferencesKey(key)] }.getOrNull()
?: strVal?.toBooleanStrictOrNull()) as T?
defaultValue is Long ->
(prefs[longPreferencesKey(key)]
(runCatching { prefs[longPreferencesKey(key)] }.getOrNull()
?: strVal?.toLongOrNull()) as T?
defaultValue is Float ->
(prefs[floatPreferencesKey(key)]
(runCatching { prefs[floatPreferencesKey(key)] }.getOrNull()
?: strVal?.toFloatOrNull()) as T?
else -> null
}
@@ -94,10 +91,10 @@ fun <T> prefDelegate(
}
}
}
}
override fun onCreate(owner: LifecycleOwner) {
appCtx.defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this)
// 注册生命周期观察者(如果有)
if (lifecycleOwner != null) {
lifecycleOwner.lifecycle.addObserver(this)
}
}
override fun onDestroy(owner: LifecycleOwner) {
@@ -105,33 +102,17 @@ fun <T> prefDelegate(
}
override fun dispose() {
appCtx.defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
dsObserverJob?.cancel()
}
@Suppress("UNCHECKED_CAST")
private fun readInitialValue(): T {
val prefs = appCtx.defaultSharedPreferences
return when {
defaultValue is String -> appCtx.getPrefString(key, defaultValue) as T
defaultValue == null && prefs.contains(key) -> {
appCtx.getPrefString(key, null) as T
}
defaultValue is Int -> appCtx.getPrefInt(key, defaultValue) as T
defaultValue is Boolean -> appCtx.getPrefBoolean(key, defaultValue) as T
defaultValue is Long -> appCtx.getPrefLong(key, defaultValue) as T
defaultValue is Float -> appCtx.getPrefFloat(key, defaultValue) as T
else -> defaultValue
}
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return _value.value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (_value.value != value) {
_value.value = value
// 同步写入 SP(向后兼容:AppConfig/MainViewModel 等仍直接读 SP
when (value) {
is String? -> if (sync) appCtx.putPrefStringSync(key, value) else appCtx.putPrefString(key, value)
is Int -> appCtx.putPrefInt(key, value)
@@ -139,8 +120,8 @@ fun <T> prefDelegate(
is Long -> appCtx.putPrefLong(key, value)
is Float -> appCtx.putPrefFloat(key, value)
}
// 同步写入 DataStore,保持 SP/DS 一致
CoroutineScope(Dispatchers.IO).launch {
// 同步写入 DataStore(权威存储),确保持久化后再返回
runBlocking(Dispatchers.IO) {
when (value) {
is String? -> DsSync.putString(key, value)
is Int -> DsSync.putInt(key, value)
@@ -149,18 +130,42 @@ fun <T> prefDelegate(
is Float -> DsSync.putFloat(key, value)
}
}
_value.value = value
onValueChange?.invoke(value)
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, changedKey: String?) {
if (changedKey == key) {
val newValue = readInitialValue()
if (_value.value != newValue) {
_value.value = newValue
onValueChange?.invoke(newValue)
}
/**
* 从 DataStore 读取当前值。
* DS 为唯一读取源,类型不匹配时安全回退到 String 解析。
*/
@Suppress("UNCHECKED_CAST")
private suspend fun readFromDs(): T? {
return try {
appCtx.dataStore.data
.catch { if (it is IOException) emit(emptyPreferences()) else throw it }
.map { prefs ->
val strVal = runCatching { prefs[stringPreferencesKey(key)] }.getOrNull()
when {
defaultValue is String || defaultValue == null ->
strVal as T?
defaultValue is Int ->
(runCatching { prefs[intPreferencesKey(key)] }.getOrNull()
?: strVal?.toIntOrNull()) as T?
defaultValue is Boolean ->
(runCatching { prefs[booleanPreferencesKey(key)] }.getOrNull()
?: strVal?.toBooleanStrictOrNull()) as T?
defaultValue is Long ->
(runCatching { prefs[longPreferencesKey(key)] }.getOrNull()
?: strVal?.toLongOrNull()) as T?
defaultValue is Float ->
(runCatching { prefs[floatPreferencesKey(key)] }.getOrNull()
?: strVal?.toFloatOrNull()) as T?
else -> null
}
}
.first()
} catch (e: Exception) {
null
}
}
}
@@ -175,4 +180,4 @@ fun <T> prefStateDelegate(
): PrefStateDelegate<T> {
val delegate = prefDelegate(key, defaultValue, lifecycleOwner, sync, onValueChange)
return PrefStateDelegate(delegate)
}
}
@@ -79,7 +79,6 @@ import io.legado.app.base.AppContextWrapper
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ThemeConfigStore
import io.legado.app.help.loadFontFiles
import io.legado.app.ui.config.labConfig.LabConfig
@@ -311,7 +310,7 @@ fun ThemeConfigScreen(
context.toastOnUi(R.string.transparent_theme_alarm)
return@ThemeColorSelector
} else {
AppConfig.containerOpacity = 0
ThemeConfig.containerOpacity = 0
}
}
val oldTheme = selectedTheme
@@ -1,19 +1,16 @@
package io.legado.app.ui.main
import android.app.Application
import android.content.SharedPreferences
import androidx.compose.runtime.Stable
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
import io.legado.app.domain.usecase.WebDavBackupUseCase
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.main.my.PrefClickEvent
import io.legado.app.utils.defaultSharedPreferences
import io.legado.app.utils.eventBus.FlowEventBus
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -21,6 +18,7 @@ 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 MainViewModel(
application: Application,
@@ -28,40 +26,25 @@ class MainViewModel(
private val webDavBackupUseCase: WebDavBackupUseCase
) : BaseViewModel(application) {
private val prefs = context.defaultSharedPreferences
private val mainPreferenceKeys = setOf(
PreferKey.showDiscovery,
PreferKey.showHome,
PreferKey.showRss,
PreferKey.showBottomView,
PreferKey.useFloatingBottomBar,
PreferKey.useFloatingBottomBarLiquidGlass,
PreferKey.defaultHomePage,
PreferKey.labelVisibilityMode,
NAV_EXTENDED_KEY
)
private val preferenceListener =
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key in mainPreferenceKeys) {
_uiState.value = readMainUiState()
}
}
private val _uiState = MutableStateFlow(readMainUiState())
val uiState = _uiState.asStateFlow()
private val _effects = MutableSharedFlow<MainEffect>(extraBufferCapacity = 8)
val effects = _effects.asSharedFlow()
init {
prefs.registerOnSharedPreferenceChangeListener(preferenceListener)
// 通过 snapshotFlow 直接观察 ThemeConfig 的 Compose State,全链路走 DS
viewModelScope.launch {
snapshotFlow {
readMainUiState()
}.collect { newState ->
if (_uiState.value != newState) {
_uiState.value = newState
}
}
}
deleteNotShelfBook()
}
override fun onCleared() {
prefs.unregisterOnSharedPreferenceChangeListener(preferenceListener)
super.onCleared()
}
fun upAllBookToc() {
FlowEventBus.post(EventBus.UP_ALL_BOOK_TOC, Unit)
}
@@ -149,31 +132,22 @@ data class MainUiState(
val navExtended: Boolean = false
)
private const val NAV_EXTENDED_KEY = "navExtended"
private fun MainViewModel.readMainUiState(): MainUiState {
val showDiscovery = context.getPrefBoolean(PreferKey.showDiscovery, true)
val showHome = context.getPrefBoolean(PreferKey.showHome, true)
val showRss = context.getPrefBoolean(PreferKey.showRss, true)
val destinations = MainDestination.mainDestinations.filter {
when (it) {
MainDestination.Explore -> showDiscovery
MainDestination.Home -> showHome
MainDestination.Rss -> showRss
MainDestination.Explore -> ThemeConfig.showDiscovery
MainDestination.Home -> ThemeConfig.showHome
MainDestination.Rss -> ThemeConfig.showRss
else -> true
}
}.toImmutableList()
return MainUiState(
destinations = destinations,
defaultHomePage = context.getPrefString(PreferKey.defaultHomePage, "bookshelf")
?: "bookshelf",
showBottomView = context.getPrefBoolean(PreferKey.showBottomView, true),
useFloatingBottomBar = context.getPrefBoolean(PreferKey.useFloatingBottomBar, false),
useFloatingBottomBarLiquidGlass = context.getPrefBoolean(
PreferKey.useFloatingBottomBarLiquidGlass,
false
),
labelVisibilityMode = context.getPrefString(PreferKey.labelVisibilityMode, "auto") ?: "auto",
navExtended = context.getPrefBoolean(NAV_EXTENDED_KEY, false)
defaultHomePage = ThemeConfig.defaultHomePage,
showBottomView = ThemeConfig.showBottomView,
useFloatingBottomBar = ThemeConfig.useFloatingBottomBar,
useFloatingBottomBarLiquidGlass = ThemeConfig.useFloatingBottomBarLiquidGlass,
labelVisibilityMode = ThemeConfig.labelVisibilityMode,
navExtended = ThemeConfig.navExtended
)
}
@@ -36,7 +36,6 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.datastore.preferences.preferencesDataStore
import androidx.preference.PreferenceManager
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import io.legado.app.R
@@ -464,8 +463,6 @@ val Context.channel: String
val Context.isDebuggable: Boolean
get() = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
val Context.dataStore by preferencesDataStore(name = "settings")
val Context.bookshelfLayoutMode: Int
get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
AppConfig.bookshelfLayoutModeLandscape
@@ -1,40 +0,0 @@
package io.legado.app.utils
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
object DataStoreUtil {
suspend fun <T> save(context: Context, key: String, value: T) {
context.dataStore.edit { preferences ->
when (value) {
is String -> preferences[stringPreferencesKey(key)] = value
is Int -> preferences[intPreferencesKey(key)] = value
is Boolean -> preferences[booleanPreferencesKey(key)] = value
is Float -> preferences[floatPreferencesKey(key)] = value
is Long -> preferences[longPreferencesKey(key)] = value
}
}
}
@Suppress("UNCHECKED_CAST")
suspend fun <T> read(context: Context, key: String, defaultValue: T): T {
return context.dataStore.data.map { preferences ->
when (defaultValue) {
is String -> preferences[stringPreferencesKey(key)] ?: defaultValue
is Int -> preferences[intPreferencesKey(key)] ?: defaultValue
is Boolean -> preferences[booleanPreferencesKey(key)] ?: defaultValue
is Float -> preferences[floatPreferencesKey(key)] ?: defaultValue
is Long -> preferences[longPreferencesKey(key)] ?: defaultValue
else -> defaultValue
} as T
}.first()
}
}