fix: dataStore逻辑

This commit is contained in:
HapeLee committed 2026-06-08 13:51:11 +08:00
1 parent b0cb83fcce
commit 9189b3dcfc
8 files changed
+83 -150

No files matched your search

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