CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Android 开发入门/课时/架构模式
RawBlame
课时目录12 节

Android 开发入门

  1. 01Android 开发概览
  2. 02四大组件
  3. 03传统 View UI
  4. 04Jetpack Compose
  5. 05架构模式
  6. 06依赖注入
  7. 07本地存储
  8. 08网络层
  9. 09异步与响应式
  10. 10Firebase 生态
  11. 11测试与质量
  12. 12综合练习

架构模式

MVVM 与 Repository。

30 分钟

架构模式

这一课你将能做到

  1. 画出 MVVM 架构图,说明 View、ViewModel、Model 三层的职责边界
  2. 实现一个完整的 Repository 层,整合网络与本地数据源
  3. 对比 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 对大多数场景足够。

讲清楚要点

  1. MVVM 三层职责:View(Compose/Fragment)只显示和转发事件;ViewModel 管理 UI 状态,处理业务逻辑;Model(Repository + DataSource)管理数据。依赖方向:View → ViewModel → Model,绝不反向。

  2. ViewModel 为什么能存活配置变更:ViewModelStore 挂在 Activity 的 NonConfigurationInstance 上,旋转时保留。但进程被杀时 ViewModel 也会销毁,需要 SavedStateHandle 持久化关键状态。

  3. Repository 是单一数据源:UI 不直接访问网络或数据库,只通过 Repository。Repository 决定数据从哪来、如何缓存、如何合并。这符合「单一可信来源」原则。

  4. Flow 优于 LiveData:Flow 是 Kotlin 协程原生,支持背压、操作符丰富、可测试性好。LiveData 是 Android 生态早期产物,新项目推荐 StateFlow/SharedFlow。

  5. 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() 不可变
忽略错误处理用户看到白屏或崩溃统一错误状态,提供重试

卡点急救

  1. 旋转后数据重新加载:ViewModel 存活但 init 里的 refresh() 又执行了。把 refresh() 移到 onStart 或加判断 if (tracks.isEmpty())。

  2. StateFlow 不更新:检查是否用 MutableStateFlow.value = ... 直接赋值。应该用 update { it.copy(...) } 或 emit(),确保生成新对象。

  3. 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 模式解决了什么问题?

    1. 单一数据源:UI 不关心数据来自网络还是数据库;2. 缓存策略集中管理;3. 离线支持:先读本地,再同步网络;4. 可测试性:Mock Repository 即可测试 ViewModel。

自测三问

  1. 为什么 ViewModel 不能直接持有 Context?如果必须用什么替代?
  2. StateFlow 和 LiveData 的核心区别是什么?为什么新项目推荐 StateFlow?
  3. 画出一个「点击收藏按钮」的完整数据流,从 Compose 到数据库。

小结一句话

MVVM 架构 = 状态驱动 UI + Repository 单一数据源 + 单向数据流;它让代码可测试、可维护,是 Android 企业级开发的标配。

延伸

  • Android 官方架构指南
  • Now in Android 示例项目
  • 下一课:依赖注入——Hilt 与 Koin 管理对象生命周期。
加载本节练习…
← 上一课Jetpack Compose下一课 →依赖注入