fix: dataStore逻辑
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user