diff --git a/app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt b/app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt index bde23ab4f..65b55b489 100644 --- a/app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt @@ -8,12 +8,6 @@ import androidx.datastore.preferences.core.* import androidx.datastore.preferences.preferencesDataStore import io.legado.app.constant.PreferKey import io.legado.app.utils.defaultSharedPreferences -import io.legado.app.utils.putPrefBoolean -import io.legado.app.utils.putPrefFloat -import io.legado.app.utils.putPrefInt -import io.legado.app.utils.putPrefLong -import io.legado.app.utils.putPrefString -import io.legado.app.utils.putPrefStringSet import io.legado.app.utils.removePref import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -35,9 +29,8 @@ val Context.dataStore: DataStore by preferencesDataStore( /** * 设置仓储 - * 采用 DataStore + SharedPreferences 桥接模式 - * 写入时同时写入两者,读取时以 DataStore 为准 - * 这样可以保持现有的基于 SP 的备份恢复功能正常工作 + * 以 DataStore 为唯一写入源,读取以 DataStore 为准。 + * SP 同步由 PrefDelegate 双写保证(阶段 1),此处不再回写 SP。 */ class SettingsRepository(private val context: Context) { @@ -58,27 +51,9 @@ class SettingsRepository(private val context: Context) { } suspend fun updatePreference(key: Preferences.Key, value: T) { - // 1. 写入 DataStore (UI 响应式来源) dataStore.edit { preferences -> preferences[key] = value } - // 2. 桥接:写回 SharedPreferences (保证备份功能正常) - syncToSharedPrefs(key.name, value) - } - - private fun syncToSharedPrefs(key: String, value: Any?) { - when (value) { - is String -> context.putPrefString(key, value) - is Int -> context.putPrefInt(key, value) - is Boolean -> context.putPrefBoolean(key, value) - is Long -> context.putPrefLong(key, value) - is Float -> context.putPrefFloat(key, value) - is Set<*> -> { - @Suppress("UNCHECKED_CAST") - context.putPrefStringSet(key, value as MutableSet) - } - null -> context.removePref(key) - } } // String 类型的快捷访问 @@ -94,9 +69,6 @@ class SettingsRepository(private val context: Context) { preferences[stringPreferencesKey(key)] = value } } - values.forEach { (key, value) -> - syncToSharedPrefs(key, value) - } } // Int 类型的快捷访问 @@ -176,7 +148,7 @@ class SettingsRepository(private val context: Context) { val editor = mutableMapOf() for (key in themeKeys) { if (!sp.contains(key)) { - val dsValue = prefs[stringPreferencesKey(key)] + val dsValue = runCatching { prefs[stringPreferencesKey(key)] }.getOrNull() if (dsValue != null) { editor[key] = dsValue needsWrite = true diff --git a/app/src/main/java/io/legado/app/help/config/DsSync.kt b/app/src/main/java/io/legado/app/help/config/DsSync.kt new file mode 100644 index 000000000..9b01a2074 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/config/DsSync.kt @@ -0,0 +1,42 @@ +package io.legado.app.help.config + +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 io.legado.app.data.repository.dataStore +import splitties.init.appCtx + +/** + * DataStore 写入辅助对象。 + * + * 让 [io.legado.app.ui.config.PrefDelegate] 在写入 SP 的同时同步写入 DataStore, + * 保证两边数据一致,避免 DataStore 迁移/读取时出现值缺失。 + */ +object DsSync { + + suspend fun putString(key: String, value: String?) { + appCtx.dataStore.edit { prefs -> + val dsKey = stringPreferencesKey(key) + if (value == null) prefs.remove(dsKey) else prefs[dsKey] = value + } + } + + suspend fun putInt(key: String, value: Int) { + appCtx.dataStore.edit { it[intPreferencesKey(key)] = value } + } + + suspend fun putBoolean(key: String, value: Boolean) { + appCtx.dataStore.edit { it[booleanPreferencesKey(key)] = value } + } + + suspend fun putLong(key: String, value: Long) { + appCtx.dataStore.edit { it[longPreferencesKey(key)] = value } + } + + suspend fun putFloat(key: String, value: Float) { + appCtx.dataStore.edit { it[floatPreferencesKey(key)] = value } + } +} diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index 9f274b552..41959ddd9 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -22,8 +22,9 @@ import io.legado.app.utils.GSON import io.legado.app.utils.LogUtils import io.legado.app.utils.compress.ZipUtils import io.legado.app.utils.createFolderIfNotExist -import io.legado.app.utils.defaultSharedPreferences +import io.legado.app.data.repository.dataStore import io.legado.app.utils.externalFiles +import kotlinx.coroutines.flow.first import io.legado.app.utils.getFile import io.legado.app.utils.isContentScheme import io.legado.app.utils.normalizeFileName @@ -177,7 +178,9 @@ object Backup { .writeText(GSON.toJson(it)) } currentCoroutineContext().ensureActive() - val configMap = appCtx.defaultSharedPreferences.all + val configMap = appCtx.dataStore.data.first() + .asMap() + .mapKeys { it.key.name } val xmlBuilder = StringBuilder() xmlBuilder.append("\n") xmlBuilder.append("\n") diff --git a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt index 363ef981a..4db827c7f 100644 --- a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt @@ -18,7 +18,18 @@ import io.legado.app.utils.putPrefInt import io.legado.app.utils.putPrefLong import io.legado.app.utils.putPrefString import io.legado.app.utils.putPrefStringSync +import io.legado.app.help.config.DsSync +import androidx.datastore.preferences.core.* +import io.legado.app.data.repository.dataStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import splitties.init.appCtx +import java.io.IOException import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -41,6 +52,7 @@ fun prefDelegate( return object : PrefDelegate, SharedPreferences.OnSharedPreferenceChangeListener, DefaultLifecycleObserver { private var _value: MutableState = mutableStateOf(readInitialValue()) override val state: State get() = _value + private var dsObserverJob: Job? = null init { if (lifecycleOwner != null) { @@ -48,6 +60,40 @@ fun prefDelegate( } else { appCtx.defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this) } + // 观察 DataStore,以 DS 为读取权威源 + dsObserverJob = CoroutineScope(Dispatchers.IO).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)] + ?: strVal?.toIntOrNull()) as T? + defaultValue is Boolean -> + (prefs[booleanPreferencesKey(key)] + ?: strVal?.toBooleanStrictOrNull()) as T? + defaultValue is Long -> + (prefs[longPreferencesKey(key)] + ?: strVal?.toLongOrNull()) as T? + defaultValue is Float -> + (prefs[floatPreferencesKey(key)] + ?: strVal?.toFloatOrNull()) as T? + else -> null + } + } + .distinctUntilChanged() + .collect { dsValue -> + if (dsValue != null && _value.value != dsValue) { + _value.value = dsValue + onValueChange?.invoke(dsValue) + } + } + } } override fun onCreate(owner: LifecycleOwner) { @@ -60,6 +106,7 @@ fun prefDelegate( override fun dispose() { appCtx.defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + dsObserverJob?.cancel() } @Suppress("UNCHECKED_CAST") @@ -92,6 +139,16 @@ fun prefDelegate( is Long -> appCtx.putPrefLong(key, value) is Float -> appCtx.putPrefFloat(key, value) } + // 同步写入 DataStore,保持 SP/DS 一致 + CoroutineScope(Dispatchers.IO).launch { + when (value) { + is String? -> DsSync.putString(key, value) + is Int -> DsSync.putInt(key, value) + is Boolean -> DsSync.putBoolean(key, value) + is Long -> DsSync.putLong(key, value) + is Float -> DsSync.putFloat(key, value) + } + } _value.value = value onValueChange?.invoke(value) }