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 androidx.datastore.preferences.preferencesDataStore
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.utils.defaultSharedPreferences
|
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 io.legado.app.utils.removePref
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
@@ -35,9 +29,8 @@ val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置仓储
|
* 设置仓储
|
||||||
* 采用 DataStore + SharedPreferences 桥接模式
|
* 以 DataStore 为唯一写入源,读取以 DataStore 为准。
|
||||||
* 写入时同时写入两者,读取时以 DataStore 为准
|
* SP 同步由 PrefDelegate 双写保证(阶段 1),此处不再回写 SP。
|
||||||
* 这样可以保持现有的基于 SP 的备份恢复功能正常工作
|
|
||||||
*/
|
*/
|
||||||
class SettingsRepository(private val context: Context) {
|
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) {
|
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) {
|
||||||
// 1. 写入 DataStore (UI 响应式来源)
|
|
||||||
dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
preferences[key] = value
|
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 类型的快捷访问
|
// String 类型的快捷访问
|
||||||
@@ -94,9 +69,6 @@ class SettingsRepository(private val context: Context) {
|
|||||||
preferences[stringPreferencesKey(key)] = value
|
preferences[stringPreferencesKey(key)] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
values.forEach { (key, value) ->
|
|
||||||
syncToSharedPrefs(key, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int 类型的快捷访问
|
// Int 类型的快捷访问
|
||||||
@@ -176,7 +148,7 @@ class SettingsRepository(private val context: Context) {
|
|||||||
val editor = mutableMapOf<String, String>()
|
val editor = mutableMapOf<String, String>()
|
||||||
for (key in themeKeys) {
|
for (key in themeKeys) {
|
||||||
if (!sp.contains(key)) {
|
if (!sp.contains(key)) {
|
||||||
val dsValue = prefs[stringPreferencesKey(key)]
|
val dsValue = runCatching { prefs[stringPreferencesKey(key)] }.getOrNull()
|
||||||
if (dsValue != null) {
|
if (dsValue != null) {
|
||||||
editor[key] = dsValue
|
editor[key] = dsValue
|
||||||
needsWrite = true
|
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.LogUtils
|
||||||
import io.legado.app.utils.compress.ZipUtils
|
import io.legado.app.utils.compress.ZipUtils
|
||||||
import io.legado.app.utils.createFolderIfNotExist
|
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 io.legado.app.utils.externalFiles
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import io.legado.app.utils.getFile
|
import io.legado.app.utils.getFile
|
||||||
import io.legado.app.utils.isContentScheme
|
import io.legado.app.utils.isContentScheme
|
||||||
import io.legado.app.utils.normalizeFileName
|
import io.legado.app.utils.normalizeFileName
|
||||||
@@ -177,7 +178,9 @@ object Backup {
|
|||||||
.writeText(GSON.toJson(it))
|
.writeText(GSON.toJson(it))
|
||||||
}
|
}
|
||||||
currentCoroutineContext().ensureActive()
|
currentCoroutineContext().ensureActive()
|
||||||
val configMap = appCtx.defaultSharedPreferences.all
|
val configMap = appCtx.dataStore.data.first()
|
||||||
|
.asMap()
|
||||||
|
.mapKeys { it.key.name }
|
||||||
val xmlBuilder = StringBuilder()
|
val xmlBuilder = StringBuilder()
|
||||||
xmlBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
|
xmlBuilder.append("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
|
||||||
xmlBuilder.append("<map>\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.putPrefLong
|
||||||
import io.legado.app.utils.putPrefString
|
import io.legado.app.utils.putPrefString
|
||||||
import io.legado.app.utils.putPrefStringSync
|
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 splitties.init.appCtx
|
||||||
|
import java.io.IOException
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
@@ -41,6 +52,7 @@ fun <T> prefDelegate(
|
|||||||
return object : PrefDelegate<T>, SharedPreferences.OnSharedPreferenceChangeListener, DefaultLifecycleObserver {
|
return object : PrefDelegate<T>, SharedPreferences.OnSharedPreferenceChangeListener, DefaultLifecycleObserver {
|
||||||
private var _value: MutableState<T> = mutableStateOf(readInitialValue())
|
private var _value: MutableState<T> = mutableStateOf(readInitialValue())
|
||||||
override val state: State<T> get() = _value
|
override val state: State<T> get() = _value
|
||||||
|
private var dsObserverJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (lifecycleOwner != null) {
|
if (lifecycleOwner != null) {
|
||||||
@@ -48,6 +60,40 @@ fun <T> prefDelegate(
|
|||||||
} else {
|
} else {
|
||||||
appCtx.defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
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) {
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
@@ -60,6 +106,7 @@ fun <T> prefDelegate(
|
|||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
appCtx.defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
appCtx.defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
|
||||||
|
dsObserverJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -92,6 +139,16 @@ 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 一致
|
||||||
|
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
|
_value.value = value
|
||||||
onValueChange?.invoke(value)
|
onValueChange?.invoke(value)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user