为什么需要架构组件
早期安卓把网络请求、数据处理、界面更新全塞进 Activity,很快就会发现两个致命问题:一是屏幕旋转重建 Activity 时数据全丢,二是 Activity 动辄几千行,改一处全身发抖。Jetpack 架构组件就是为解决这两件事而生。
它的核心思想是把"界面"和"数据"分开:ViewModel 持有数据与业务逻辑,不随屏幕旋转销毁;界面只负责观察数据变化并渲染。配合 LiveData 或 Flow,数据一变,界面自动刷新,你再也不用手动去 findViewById 更新控件。
架构组件的价值不是"多写几个类",而是让 Activity 瘦下来、让数据在配置变更中活下来。这两点做到了,代码质量会上一个台阶。
ViewModel:生命周期感知的数据持有者
ViewModel 的生命周期比 Activity 长:屏幕旋转时 Activity 被销毁重建,ViewModel 却保留下来,重建后拿到的还是同一个实例。这就是旋转后数据不丢的原理。
class UserViewModel : ViewModel() {
private val _name = MutableStateFlow("")
val name: StateFlow<String> = _name.asStateFlow()
fun onNameChange(input: String) {
_name.value = input.trim()
}
override fun onCleared() {
super.onCleared()
// 页面真正销毁时在这里释放资源
}
}
两条铁律:ViewModel 里绝不能持有 Activity、View、Context 这类短生命周期引用,否则会内存泄漏;耗时任务用 viewModelScope 启动,页面销毁时自动取消,不会回调到已销毁的界面。
LiveData:可观察的数据容器
LiveData 是一个可观察的数据持有者,它自带生命周期感知:只有界面处于前台时才把数据推给观察者,避免界面不可见时的无用回调与崩溃。
- MutableLiveData私有可写,对外只暴露只读的 LiveData,防止外部随意改值。
- observe传 LifecycleOwner,数据只在界面可见时回调。
- liveData 构建器可用 liveData{} 协程块,把异步结果自动转成 LiveData。
- 与 Flow 取舍新项目优先 Flow,LiveData 多用于老代码兼容。
用 LiveData 时坚持"对外只读"原则:内部用 MutableLiveData 改值,暴露给界面的是只读 LiveData。这样数据的修改入口唯一,状态才不会被改乱。
Kotlin Flow:更现代的数据流
Flow 是 Kotlin 协程生态里的响应式流,能力比 LiveData 更强:可以做变换、合并、节流,能发射多个连续值。在 ViewModel 里最常用 StateFlow 持有界面状态。
class SearchViewModel : ViewModel() {
private val query = MutableStateFlow("")
// 输入变化 → 防抖 300ms → 触发搜索,典型的数据流串联
val results: StateFlow<List<Item>> = query
.debounce(300)
.distinctUntilChanged()
.mapLatest { repo.search(it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
几个关键操作符:debounce 防抖、distinctUntilChanged 去重、mapLatest 取消过期请求、stateIn 转成可观察状态流。把这些串起来,搜索框这种交互就不用自己写一堆计时器了。
界面状态:用密封类表达加载态
一个页面的数据状态通常是"加载中 / 成功 / 失败"三选一。用密封类把它表达出来,界面层用 when 穷尽处理,就不会出现"成功了却忘了隐藏加载圈"这类漏处理。
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val msg: String) : UiState<Nothing>
}
// 界面侧
when(state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showContent(state.data)
is UiState.Error -> showError(state.msg)
}
这种写法的好处是编译器帮你检查分支是否齐全。以后加一种状态(比如空数据),所有 when 都会报出漏处理的位置,这正是密封类的价值。
Repository 分层与依赖
ViewModel 不直接碰网络或数据库,中间隔一层 Repository。Repository 决定数据从哪来:优先用缓存,缓存过期再拉网络,对外只暴露一个干净的接口。
- 界面 → ViewModel:只观察状态,不关心数据来源。
- ViewModel → Repository:发起请求,拿到 UiState。
- Repository → 数据源:本地数据库或网络 API。
这样分层后,换数据源、加缓存、做单元测试都只动 Repository,界面和 ViewModel 不用改。依赖方向永远是从外向内,不要让 ViewModel 知道网络细节。
实战:一个可复用的加载页
把上面的模式拼成一个完整页面:ViewModel 暴露 UiState,Activity 观察并渲染。
class NewsViewModel(private val repo: NewsRepository = NewsRepository()) : ViewModel() {
private val _state = MutableStateFlow<UiState<List<News>>>(UiState.Loading)
val state: StateFlow<UiState<List<News>>> = _state.asStateFlow()
fun load() = viewModelScope.launch {
_state.value = UiState.Loading
_state.value = try {
UiState.Success(repo.fetchNews())
} catch (e: Exception) {
UiState.Error("加载失败:${e.message}")
}
}
}
// Activity 中:生命周期感知地收集状态
lifecycleScope.launch {
viewModel.state.collect { render(it) }
}
这段代码把密封类状态、StateFlow、viewModelScope 三件套固定下来后,团队每个列表页都长一个样子,新人接手几乎零成本。这就是架构组件真正的回报。
常见坑与最佳实践
最后整理高频问题。
- ViewModel 持有 Context:导致内存泄漏,要用 Application Context 或注入。
- 在 GlobalScope 启协程:不受生命周期管理,必泄漏,用 viewModelScope。
- 数据流重复订阅:用 stateIn 的 SharingStarted.WhileSubscribed 避免多页面重复拉数据。
- 状态直接暴露可变对象:对外给只读 StateFlow,修改入口收在 ViewModel。
- 跳过 Repository:ViewModel 直接调接口,后续加缓存要到处改。
架构组件不是教条,而是把"界面瘦、数据活、状态单一"这三件事固化成默认写法。守住这三点,怎么组织类都不会太差。
下一步建议结合下一篇的网络与本地存储,把 Repository 的真实数据源补齐,那才是一个能上线的完整闭环。