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刷新。
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
)
}
}
setDefaultNightMode()需要在setContentView()之前调用才能生效。所以上面代码中,我先观察LiveData并应用主题,再调用super.onCreate()。这是一个容易踩坑的地方,我曾经因为这个折腾了半天。
8.3 实战:完整的主题切换流程
我们来梳理一下整个流程,用一张图来展示:
流程其实很简单:用户操作 → 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