8. LiveData与SharedPreferences:封装SharedPreferences为LiveData、实现设置页面的实时响应、实战:主题切换

大家好,我是你们的老朋友。今天我们来聊一个非常接地气的话题——怎么把SharedPreferences和LiveData结合起来。

说实话,我刚入行那会儿,设置页面的数据同步问题没少让我头疼。用户改了个主题,切回主界面发现没变化,非得重启App才行。这种体验,说白了就是「反人类」。后来我意识到,问题的根源在于:SharedPreferences本身不具备「观察者模式」的能力。

那怎么办?LiveData就是来救场的。

8.1 为什么需要把SharedPreferences封装成LiveData?

SharedPreferences是Android里最常用的轻量级存储方案。但它有个硬伤——你存了数据,不会主动通知UI更新。你想想看,用户改了个设置,UI还停留在旧状态,这合理吗?

LiveData正好弥补这个缺口。它是「可观察的数据持有者」,UI只要订阅了它,数据一变,界面自动刷新。把两者结合,就等于给SharedPreferences装上了「实时通知」的能力。

我在项目中遇到过这样一个场景:用户切换夜间模式,设置页面需要立刻响应,同时主界面也要同步变化。如果没有LiveData,你得手动写一堆回调,代码又臭又长。有了它,一切变得优雅。

8.2 核心思路:封装一个LiveData版本的SharedPreferences

封装的核心其实就一句话:监听SharedPreferences的变化,然后通过LiveData发射出去

Android的SharedPreferences本身就提供了OnSharedPreferenceChangeListener接口。我们只需要在LiveData内部注册这个监听器,当数据变化时,调用postValue()setValue()即可。

嗯,这里要注意一点:LiveData的setValue()必须在主线程调用,而postValue()可以在任何线程。SharedPreferences的回调是在主线程的,所以用setValue()就够了。

8.2.1 基础封装:SharedPreferencesLiveData

我们先写一个通用的封装类。我习惯把它设计成泛型,这样不管是String、Int还是Boolean,都能复用。

class SharedPreferenceLiveData<T>(
    private val sharedPreferences: SharedPreferences,
    private val key: String,
    private val defaultValue: T
) : LiveData<T>() {

    private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey ->
        if (changedKey == key) {
            val value = getValueFromPreferences()
            // 只在值真正变化时才更新
            if (value != this.value) {
                setValue(value)
            }
        }
    }

    override fun onActive() {
        super.onActive()
        // 首次激活时,先把当前值发出去
        setValue(getValueFromPreferences())
        sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
    }

    override fun onInactive() {
        super.onInactive()
        sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
    }

    @Suppress("UNCHECKED_CAST")
    private fun getValueFromPreferences(): T {
        return when (defaultValue) {
            is String -> sharedPreferences.getString(key, defaultValue as String) as T
            is Int -> sharedPreferences.getInt(key, defaultValue as Int) as T
            is Boolean -> sharedPreferences.getBoolean(key, defaultValue as Boolean) as T
            is Float -> sharedPreferences.getFloat(key, defaultValue as Float) as T
            is Long -> sharedPreferences.getLong(key, defaultValue as Long) as T
            else -> throw IllegalArgumentException("不支持的泛型类型")
        }
    }
}

这段代码有几个关键点:

  • onActive():当有观察者订阅时,注册监听器并立即发射当前值。这样UI一绑定就能拿到最新数据。
  • onInactive():没有观察者时,及时注销监听器,避免内存泄漏。
  • 值变化判断:我加了一个if (value != this.value)的判断,防止重复发射相同的值。这在某些场景下能避免不必要的UI刷新。
小技巧: 如果你需要支持更多类型(比如Set<String>),可以在when分支里继续扩展。不过我个人建议,复杂类型还是用Room或者DataStore更合适。

8.2.2 使用示例:主题切换的实时响应

有了上面的封装,实现主题切换就变得非常简单了。我们来看一个实战例子。

假设我们有一个设置页面,用户可以切换「深色模式」。我们需要把这个开关状态存到SharedPreferences里,并且让整个App都能实时感知到变化。

// 定义一个主题管理类
object ThemeManager {
    private const val KEY_DARK_MODE = "dark_mode"
    
    lateinit var darkModeLiveData: SharedPreferenceLiveData<Boolean>
        private set

    fun init(context: Context) {
        val prefs = PreferenceManager.getDefaultSharedPreferences(context)
        darkModeLiveData = SharedPreferenceLiveData(prefs, KEY_DARK_MODE, false)
    }

    fun setDarkMode(enabled: Boolean) {
        darkModeLiveData.sharedPreferences
            .edit()
            .putBoolean(KEY_DARK_MODE, enabled)
            .apply()
        // 注意:不需要手动通知LiveData,因为监听器会自动触发
    }
}

然后在Activity或Fragment里观察这个LiveData:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // 在setContentView之前应用主题
        ThemeManager.darkModeLiveData.observe(this) { isDarkMode ->
            applyTheme(isDarkMode)
        }
        
        // 获取当前值并应用
        val currentMode = ThemeManager.darkModeLiveData.value ?: false
        applyTheme(currentMode)
        
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    private fun applyTheme(isDarkMode: Boolean) {
        // 这里可以设置DayNight主题
        AppCompatDelegate.setDefaultNightMode(
            if (isDarkMode) AppCompatDelegate.MODE_NIGHT_YES
            else AppCompatDelegate.MODE_NIGHT_NO
        )
    }
}
注意: 在Activity中,setDefaultNightMode()需要在setContentView()之前调用才能生效。所以上面代码中,我先观察LiveData并应用主题,再调用super.onCreate()。这是一个容易踩坑的地方,我曾经因为这个折腾了半天。

8.3 实战:完整的主题切换流程

我们来梳理一下整个流程,用一张图来展示:

主题切换实时响应流程 用户切换开关 SharedPreferences LiveData UI自动刷新 监听器自动捕获变化 无需手动调用postValue

流程其实很简单:用户操作 → SharedPreferences存储 → 监听器触发 → LiveData发射新值 → UI自动刷新。整个链条是自动化的,你只需要在UI层观察LiveData即可。

8.4 进阶:避免重复发射相同值

刚才的代码里,我加了一个if (value != this.value)的判断。为什么要这么做?

你想想看,如果用户连续两次点击同一个开关,第一次从false变成true,第二次又从true变成true(虽然逻辑上不会,但代码层面可能发生)。如果没有这个判断,LiveData会发射两次true,UI就会刷新两次。虽然大多数情况下没问题,但如果你的UI刷新逻辑比较重(比如重建Activity),就会造成不必要的性能开销。

我曾经在一个项目中遇到过这个问题:用户快速切换主题,导致Activity被反复重建,界面闪烁严重。加上这个判断后,问题迎刃而解。

8.5 封装成工具类:更优雅的使用方式

在实际项目中,我们通常不会直接使用SharedPreferenceLiveData,而是再封装一层,方便全局调用。

class PreferenceManager(private val context: Context) {
    private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
    
    val isDarkMode: SharedPreferenceLiveData<Boolean> by lazy {
        SharedPreferenceLiveData(prefs, "dark_mode", false)
    }
    
    val fontSize: SharedPreferenceLiveData<Int> by lazy {
        SharedPreferenceLiveData(prefs, "font_size", 16)
    }
    
    fun setDarkMode(enabled: Boolean) {
        prefs.edit().putBoolean("dark_mode", enabled).apply()
    }
    
    fun setFontSize(size: Int) {
        prefs.edit().putInt("font_size", size).apply()
    }
}

然后在Application中初始化:

class MyApp : Application() {
    lateinit var preferenceManager: PreferenceManager
        private set
    
    override fun onCreate() {
        super.onCreate()
        preferenceManager = PreferenceManager(this)
    }
}

这样,在任何地方都可以通过(application as MyApp).preferenceManager.isDarkMode来观察主题变化了。

核心要点回顾:
  • SharedPreferences本身不具备观察能力,需要借助LiveData实现实时响应
  • 封装的关键是注册OnSharedPreferenceChangeListener,在onActive()中注册,在onInactive()中注销
  • 使用泛型封装可以支持多种数据类型
  • 添加值变化判断可以避免重复发射,优化性能
  • 主题切换的实战中,注意setDefaultNightMode()的调用时机

好了,关于LiveData与SharedPreferences的封装就讲到这里。这个模式在设置页面中非常实用,尤其是主题切换、字体大小、语言选择等场景。你可以在自己的项目里试试看,相信会给你带来不一样的体验。


公众号:蓝海资料掘金营,微信deep3321