Эх сурвалжийг харах

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 避免类型不匹配崩溃
HapeLee 5 сар өмнө
parent
commit
2d4372f968

+ 3 - 31
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<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

+ 42 - 0
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 }
+    }
+}

+ 5 - 2
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("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
         xmlBuilder.append("<map>\n")

+ 57 - 0
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 <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)
             }