refactor: SP/DS 统一迁移 — PrefDelegate 双写 + DS 观察者 + 备份从 DS 读取
Phase 1: PrefDelegate.setValue 写入 SP 后异步同步到 DataStore (DsSync) Phase 2: PrefDelegate.init 启动 DS 观察者,以 DS 为读取权威源,含类型回退 Phase 3: SettingsRepository 移除 syncToSharedPrefs,DS 为唯一写入源 Phase 4: Backup.kt 从 DataStore 读取配置(替代 defaultSharedPreferences) 修复: - postMigrationSync 读取 DS String key 时 ClassCastException (runCatching) - DS 观察者类型回退: typed key 读不到时从 stringPreferencesKey 转换 - DS 观察者安全读取: runCatching 避免类型不匹配崩溃
This commit is contained in:
@@ -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<Preferences> 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 <T> updatePreference(key: Preferences.Key<T>, 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<String>)
|
||||
}
|
||||
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<String, String>()
|
||||
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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
|
||||
xmlBuilder.append("<map>\n")
|
||||
|
||||
@@ -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 <T> prefDelegate(
|
||||
return object : PrefDelegate<T>, SharedPreferences.OnSharedPreferenceChangeListener, DefaultLifecycleObserver {
|
||||
private var _value: MutableState<T> = mutableStateOf(readInitialValue())
|
||||
override val state: State<T> get() = _value
|
||||
private var dsObserverJob: Job? = null
|
||||
|
||||
init {
|
||||
if (lifecycleOwner != null) {
|
||||
@@ -48,6 +60,40 @@ fun <T> 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 <T> prefDelegate(
|
||||
|
||||
override fun dispose() {
|
||||
appCtx.defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||
dsObserverJob?.cancel()
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -92,6 +139,16 @@ fun <T> 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user