大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
架构模式
这一课你将能做到
- 画出 MVVM 架构图,说明 View、ViewModel、Model 三层的职责边界
- 实现一个完整的 Repository 层,整合网络与本地数据源
- 对比 MVP、MVVM、MVI 的优劣,并解释为什么 Google 推荐 MVVM
为什么要学
没有架构的代码是「意大利面条」:Activity 里塞网络请求、数据库操作、业务逻辑,几千行后没人能改。架构模式不是银弹,但它是「如何让代码可测试、可维护、可扩展」的共识方案。对于 Web 前端,MVVM 就像 React + Redux + Service 层的组合。
人话:架构就是「分锅」。View 只负责显示,ViewModel 只负责状态,Model 只负责数据。出了问题,你知道去哪层找。代码评审时,你知道该骂谁。
面试中,「MVVM 各层职责」、「ViewModel 为什么能存活配置变更」、「Repository 模式的作用」是必考题。
先建立图像
┌─────────────────────────────────────────┐
│ UI 层 │
│ ┌─────────┐ ┌─────────┐ │
│ │ Compose │ │ Fragment│ ← 只显示 │
│ │ 页面 │ │ 页面 │ 和交互 │
│ └────┬────┘ └────┬────┘ │
│ └─────────────┘ │
│ ↓ StateFlow │
│ ┌─────────────────────┐ │
│ │ ViewModel │ ← 状态管理 │
│ │ (业务逻辑/状态保存) │ 和逻辑 │
│ └──────────┬──────────┘ │
│ ↓ 调用 │
│ ┌─────────────────────┐ │
│ │ Repository │ ← 数据协调 │
│ │ (单一数据源入口) │ 和缓存 │
│ └──────────┬──────────┘ │
│ ↓ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Retrofit│ │ Room │ │DataStore│ │
│ │ (网络) │ │ (数据库)│ │ (偏好) │ │
│ └────────┘ └────────┘ └────────┘ │
└─────────────────────────────────────────┘
跟着做
步骤 1:定义数据模型
// 网络层 DTO
data class TrackDto(
val slug: String,
val title: String,
val description: String,
val lesson_count: Int,
val updated_at: String
)
// 数据库 Entity
@Entity(tableName = "tracks")
data class TrackEntity(
@PrimaryKey val slug: String,
val title: String,
val description: String,
val lessonCount: Int,
val updatedAt: String,
val isFavorite: Boolean = false
)
// UI 层 Model
data class TrackUiModel(
val slug: String,
val title: String,
val description: String,
val lessonCount: Int,
val progress: Int,
val isFavorite: Boolean
)
步骤 2:Mapper 层转换
fun TrackDto.toEntity(): TrackEntity = TrackEntity(
slug = slug,
title = title,
description = description,
lessonCount = lesson_count,
updatedAt = updated_at
)
fun TrackEntity.toUiModel(progress: Int = 0): TrackUiModel = TrackUiModel(
slug = slug,
title = title,
description = description,
lessonCount = lessonCount,
progress = progress,
isFavorite = isFavorite
)
步骤 3:Repository 整合数据源
class TrackRepository @Inject constructor(
private val api: TrackApiService,
private val dao: TrackDao,
private val progressStore: ProgressDataStore
) {
// 暴露给 ViewModel 的是 Flow,自动更新
val tracks: Flow<List<TrackUiModel>> = combine(
dao.getAllTracks(),
progressStore.progressMap
) { entities, progressMap ->
entities.map { entity ->
entity.toUiModel(progress = progressMap[entity.slug] ?: 0)
}
}
// 刷新:网络 → 数据库
suspend fun refreshTracks(): Result<Unit> = withContext(Dispatchers.IO) {
try {
val response = api.getTracks()
val entities = response.data.map { it.toEntity() }
dao.insertAll(entities)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun toggleFavorite(slug: String) {
dao.toggleFavorite(slug)
}
suspend fun getTrackDetail(slug: String): TrackUiModel? {
return dao.getTrackBySlug(slug)?.toUiModel()
}
}
步骤 4:ViewModel 管理状态
@HiltViewModel
class TrackListViewModel @Inject constructor(
private val repository: TrackRepository
) : ViewModel() {
// UI 状态:不可变数据类
data class UiState(
val tracks: List<TrackUiModel> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null,
val isRefreshing: Boolean = false
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
init {
observeTracks()
refresh()
}
private fun observeTracks() {
viewModelScope.launch {
repository.tracks.collect { tracks ->
_uiState.update { it.copy(tracks = tracks) }
}
}
}
fun refresh() {
viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true, error = null) }
repository.refreshTracks()
.onSuccess {
_uiState.update { it.copy(isRefreshing = false) }
}
.onFailure { e ->
_uiState.update {
it.copy(
isRefreshing = false,
error = e.message ?: "未知错误"
)
}
}
}
}
fun onFavoriteClick(slug: String) {
viewModelScope.launch {
repository.toggleFavorite(slug)
}
}
fun clearError() {
_uiState.update { it.copy(error = null) }
}
}
步骤 5:Compose 页面消费状态
@Composable
fun TrackListScreen(
viewModel: TrackListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(title = { Text("学习路线") })
}
) { padding ->
Box(modifier = Modifier.padding(padding)) {
when {
uiState.isLoading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
uiState.error != null -> {
ErrorMessage(
message = uiState.error!!,
onRetry = viewModel::refresh,
onDismiss = viewModel::clearError
)
}
else -> {
TrackList(
tracks = uiState.tracks,
isRefreshing = uiState.isRefreshing,
onRefresh = viewModel::refresh,
onFavoriteClick = viewModel::onFavoriteClick
)
}
}
}
}
}
@Composable
fun TrackList(
tracks: List<TrackUiModel>,
isRefreshing: Boolean,
onRefresh: () -> Unit,
onFavoriteClick: (String) -> Unit
) {
PullToRefreshContainer(
isRefreshing = isRefreshing,
onRefresh = onRefresh
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(tracks, key = { it.slug }) { track ->
TrackItem(
track = track,
onFavoriteClick = { onFavoriteClick(track.slug) }
)
}
}
}
}
步骤 6:MVI 变体(可选了解)
MVI(Model-View-Intent)强调单向数据流:
// Intent:用户意图
sealed class TrackListIntent {
object Refresh : TrackListIntent()
data class ToggleFavorite(val slug: String) : TrackListIntent()
}
// ViewModel 处理 Intent
fun processIntent(intent: TrackListIntent) {
when (intent) {
is TrackListIntent.Refresh -> refresh()
is TrackListIntent.ToggleFavorite -> toggleFavorite(intent.slug)
}
}
MVI 更严格,适合复杂交互场景,但样板代码更多。MVVM 对大多数场景足够。
讲清楚要点
-
MVVM 三层职责:View(Compose/Fragment)只显示和转发事件;ViewModel 管理 UI 状态,处理业务逻辑;Model(Repository + DataSource)管理数据。依赖方向:View → ViewModel → Model,绝不反向。
-
ViewModel 为什么能存活配置变更:
ViewModelStore挂在 Activity 的NonConfigurationInstance上,旋转时保留。但进程被杀时 ViewModel 也会销毁,需要SavedStateHandle持久化关键状态。 -
Repository 是单一数据源:UI 不直接访问网络或数据库,只通过 Repository。Repository 决定数据从哪来、如何缓存、如何合并。这符合「单一可信来源」原则。
-
Flow 优于 LiveData:Flow 是 Kotlin 协程原生,支持背压、操作符丰富、可测试性好。LiveData 是 Android 生态早期产物,新项目推荐 StateFlow/SharedFlow。
-
Udf 单向数据流:状态只从 ViewModel 流向 View,事件只从 View 流向 ViewModel。禁止 View 直接修改 ViewModel 状态,必须通过方法调用。
你可能会懵的地方
-
ViewModel 里能不能放 Context? 绝对不行。ViewModel 生命周期比 Activity 长,持有 Context 会导致内存泄漏。需要 Context 时,用
Application或通过 DI 注入。 -
Repository 需要是单例吗? 通常是。用
@Singleton注解,确保全 App 共享一份数据缓存。但某些场景(如多用户切换)可能需要按用户隔离。 -
UseCase/Interactor 有必要吗? 简单项目可以没有。当业务逻辑复杂(如「同步数据 + 检查权限 + 记录日志」)时,UseCase 把 ViewModel 从具体业务中解放出来。
-
MVI 和 MVVM 怎么选? MVI 更严格,状态不可变、事件单向,适合复杂表单、实时协作。MVVM 更灵活,适合大多数 CRUD 应用。Google 官方示例用 MVVM。
-
Compose 还需要 Fragment 吗? 单 Activity 多 Compose 页面是趋势。Fragment 只在需要特定生命周期(如 ViewPager2、Dialog)或与旧代码互操作时使用。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| Activity 直接访问网络 | 无法测试,生命周期泄漏 | Repository 封装,ViewModel 调度 |
| ViewModel 持有 View 引用 | 内存泄漏 | 只暴露 StateFlow/LiveData |
| 在 Composable 里直接写业务逻辑 | 无法复用,难测试 | 逻辑下沉到 ViewModel |
用 var 定义 UiState | 状态可被外部修改,不可预测 | data class + copy() 不可变 |
| 忽略错误处理 | 用户看到白屏或崩溃 | 统一错误状态,提供重试 |
卡点急救
-
旋转后数据重新加载:ViewModel 存活但
init里的refresh()又执行了。把refresh()移到onStart或加判断if (tracks.isEmpty())。 -
StateFlow 不更新:检查是否用
MutableStateFlow.value = ...直接赋值。应该用update { it.copy(...) }或emit(),确保生成新对象。 -
Hilt 注入失败:检查是否忘加
@HiltAndroidApp到 Application,或@AndroidEntryPoint到 Activity/Fragment。
面试常问
-
MVVM 各层如何通信? View 观察 ViewModel 的 StateFlow/LiveData;ViewModel 调用 Repository 的 suspend 函数或 Flow;Repository 操作具体 DataSource。事件方向:View → ViewModel → Repository;数据方向:Repository → ViewModel → View。
-
ViewModel 的生命周期? 与 Activity/Fragment 的
ViewModelStore绑定。配置变更时存活,Activity 真正销毁(finish)或进程被杀时销毁。onCleared()是清理资源的时机。 -
Repository 模式解决了什么问题?
- 单一数据源:UI 不关心数据来自网络还是数据库;2. 缓存策略集中管理;3. 离线支持:先读本地,再同步网络;4. 可测试性:Mock Repository 即可测试 ViewModel。
自测三问
- 为什么 ViewModel 不能直接持有 Context?如果必须用什么替代?
- StateFlow 和 LiveData 的核心区别是什么?为什么新项目推荐 StateFlow?
- 画出一个「点击收藏按钮」的完整数据流,从 Compose 到数据库。
小结一句话
MVVM 架构 = 状态驱动 UI + Repository 单一数据源 + 单向数据流;它让代码可测试、可维护,是 Android 企业级开发的标配。
延伸
- Android 官方架构指南
- Now in Android 示例项目
- 下一课:依赖注入——Hilt 与 Koin 管理对象生命周期。