当你在开发一个天气预报应用时,用户刚输入完城市名称,突然旋转屏幕——所有输入内容瞬间消失。这种糟糕的体验正是Android配置变更带来的典型问题。作为开发者,我们需要确保用户数据在屏幕旋转等配置变更时不会丢失,同时避免内存泄漏风险。
Android设备在屏幕旋转、语言切换等配置变更时,系统会销毁并重建当前Activity。传统的数据保存方式主要有两种:
ViewModel的出现提供了第三种更优雅的方案。它能在配置变更时自动保留数据,同时在Activity真正销毁时自动清理,完美平衡了数据持久化与内存管理。
kotlin复制// 传统方式保存数据
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("city", cityName)
}
// 传统方式恢复数据
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
cityName = savedInstanceState?.getString("city") ?: ""
}
ViewModel与LiveData组合使用时,数据持久化能力更加强大:
| 特性 | ViewModel+LiveData | onSaveInstanceState | 持久化存储 |
|---|---|---|---|
| 数据保留范围 | 配置变更期间 | 配置变更期间 | 长期 |
| 适合数据类型 | 任意对象 | 简单类型 | 任意数据 |
| 内存占用 | 中等 | 最小 | 最大 |
| 读写性能 | 最佳 | 好 | 较差 |
| 生命周期感知 | 是 | 否 | 否 |
ViewModel之所以能存活于配置变更,关键在于其生命周期比Activity更长。它由ViewModelStoreOwner(通常是ComponentActivity)管理,只在所有者完全销毁时才会清除。
典型错误实现:
kotlin复制// 错误!直接持有Activity引用会导致内存泄漏
class MyViewModel(activity: Activity) : ViewModel() {
private val context = activity
}
正确实现方式:
kotlin复制class WeatherViewModel : ViewModel() {
private val _cityName = MutableLiveData<String>()
val cityName: LiveData<String> = _cityName
fun setCity(name: String) {
_cityName.value = name
}
override fun onCleared() {
// 清理资源
super.onCleared()
}
}
在Activity中使用ViewModelProvider获取实例:
kotlin复制class WeatherActivity : AppCompatActivity() {
private lateinit var viewModel: WeatherViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(this).get(WeatherViewModel::class.java)
viewModel.cityName.observe(this) { name ->
cityInput.setText(name)
}
}
}
提示:ViewModel绝对不应该持有View、Activity或Fragment的引用,这是避免内存泄漏的关键原则。
LiveData不仅是数据容器,更是生命周期感知的观察者模式实现。合理使用可以大幅简化UI更新逻辑。
数据转换操作:
kotlin复制val weatherData: LiveData<Weather> = repository.getWeather()
val temperatureText: LiveData<String> = Transformations.map(weatherData) {
"${it.temperature}°C"
}
多数据源合并:
kotlin复制val currentWeather = repository.getCurrentWeather()
val forecast = repository.getForecast()
val combinedData = MediatorLiveData<Pair<Weather?, Forecast?>>().apply {
addSource(currentWeather) { value = it to forecast.value }
addSource(forecast) { value = currentWeather.value to it }
}
常见陷阱与解决方案:
重复订阅问题:
kotlin复制// 错误!每次配置变更都会创建新观察者
viewModel.data.observe(this) { updateUI(it) }
// 正确!使用lifecycleOwner的viewLifecycleOwner(在Fragment中)
viewModel.data.observe(viewLifecycleOwner) { updateUI(it) }
数据倒灌问题:
kotlin复制private val _events = MutableLiveData<Event>()
val events: LiveData<Event> = _events
fun triggerEvent() {
_events.value = Event() // 使用value设置会立即通知观察者
}
// 解决方案:使用SingleLiveEvent或Event Wrapper
在真实项目开发中,我们往往需要处理更复杂的场景:
网络请求与状态管理:
kotlin复制class WeatherViewModel(repository: WeatherRepository) : ViewModel() {
private val _state = MutableLiveData<State>()
val state: LiveData<State> = _state
fun fetchWeather(city: String) {
_state.value = State.Loading
viewModelScope.launch {
try {
val data = repository.fetchWeather(city)
_state.value = State.Success(data)
} catch (e: Exception) {
_state.value = State.Error(e)
}
}
}
sealed class State {
object Loading : State()
data class Success(val data: Weather) : State()
data class Error(val exception: Exception) : State()
}
}
多Fragment共享数据:
kotlin复制// 在Activity范围内共享ViewModel
val sharedViewModel: SharedViewModel by activityViewModels()
// 在Fragment中观察相同实例
sharedViewModel.sharedData.observe(viewLifecycleOwner) { data ->
// 更新UI
}
数据库与网络数据同步:
kotlin复制fun getWeather(city: String): LiveData<Weather> {
// 先显示本地数据
val local = localDataSource.getWeather(city)
// 同时发起网络请求更新
viewModelScope.launch {
try {
val remote = remoteDataSource.fetchWeather(city)
localDataSource.save(remote)
} catch (e: Exception) {
// 处理错误
}
}
return local
}
确保ViewModel和LiveData高效运行需要注意以下要点:
内存优化检查表:
调试工具:
bash复制# 查看ViewModel实例
adb shell dumpsys activity | grep "ViewModelStore"
常见问题排查:
数据不更新:
内存泄漏:
数据重复:
在实际项目中,我遇到过一个棘手案例:当快速旋转屏幕时,多个网络请求同时发起导致数据混乱。最终通过为每个请求添加唯一ID,并在ViewModel中只处理最新请求的方式解决了问题。