首页 / Android 入门教程 / MVVM 架构

Android 入门教程

MVVM 架构

本教程共 100 篇 · 第 81 篇 · 更新于 2026-07-28 · 约 9 分钟阅读

AndroidAndroid 入门教程MVVM架构RepositoryJetpackCompose

81. MVVM 架构

本节目标:理解 MVVM 三层的职责划分,学会用 ViewModel + StateFlow + Repository 搭一个完整的 MVVM 实现,落地单向数据流,配合 Compose 状态提升写好界面层。

前面几章分别讲了架构原则、ViewModel、LiveData/Lifecycle。这章把它们串起来,看 MVVM 在 Android 上怎么落地。

为什么是 MVVM

Android 历史上出现过几种架构:

  • MVC:Model-View-Controller。Android 里 Activity 既是 View 又是 Controller,职责混乱,难以测试。
  • MVP:Model-View-Presenter。Presenter 通过接口回调更新 View,接口多,代码啰嗦,且 Presenter 持有 View 引用容易泄漏。
  • MVVM:Model-View-ViewModel。ViewModel 暴露状态,View 观察状态自动更新。ViewModel 不持有 View 引用,安全可测。

官方推荐 MVVM,因为:

  • ViewModel 不持有 View,无泄漏风险。
  • 状态驱动 UI,配合 Compose 声明式天然契合。
  • 配置变化时 ViewModel 保留,状态不丢。
  • 业务逻辑集中在 ViewModel,可测试。

MVVM 三层职责

Model(数据层)

提供数据访问。包括:

  • Repository:汇聚数据源,决定从哪取数据。
  • DataSource:具体来源,如网络、数据库、文件、缓存。
  • 数据模型:纯数据类,表示业务实体。

Model 层不知道 UI 的存在,只负责数据。

data class Note(val id: Long, val title: String, val content: String)

class NoteRepository(
    private val dao: NoteDao,
    private val api: NoteApi
) {
    fun observeNotes(): Flow<List<Note>> = dao.observeAll()

    suspend fun refresh() {
        val notes = api.listNotes()
        dao.insertAll(notes)
    }

    suspend fun addNote(title: String, content: String): Long {
        val note = Note(0, title, content)
        val id = dao.insert(note)
        api.createNote(note.copy(id = id))
        return id
    }
}

ViewModel(逻辑层 + 状态容器)

持有 UI 状态,处理业务逻辑,调用 Repository。

class NoteViewModel(private val repo: NoteRepository) : ViewModel() {

    private val _uiState = MutableStateFlow<NoteUiState>(NoteUiState.Loading)
    val uiState: StateFlow<NoteUiState> = _uiState.asStateFlow()

    private val _events = MutableSharedFlow<String>()
    val events = _events.asSharedFlow()

    init {
        loadNotes()
    }

    private fun loadNotes() {
        viewModelScope.launch {
            repo.observeNotes().collect { notes ->
                _uiState.value = if (notes.isEmpty()) {
                    NoteUiState.Empty
                } else {
                    NoteUiState.Success(notes)
                }
            }
        }
    }

    fun refresh() {
        viewModelScope.launch {
            try {
                repo.refresh()
                _events.emit("刷新成功")
            } catch (e: Exception) {
                _events.emit("刷新失败:${e.message}")
            }
        }
    }

    fun addNote(title: String, content: String) {
        viewModelScope.launch {
            repo.addNote(title, content)
            _events.emit("已添加")
        }
    }
}

sealed interface NoteUiState {
    object Loading : NoteUiState
    object Empty : NoteUiState
    data class Success(val notes: List<Note>) : NoteUiState
}

要点:

  • 对外暴露不可变的 StateFlow<UiState>,UI 只读不写。
  • 业务事件用 SharedFlow,一次性通知。
  • 所有数据操作走协程,viewModelScope 自动管理。
  • ViewModel 不持有任何 UI 引用。

View(界面层)

观察 ViewModel 的状态,渲染 UI,把用户事件传给 ViewModel。

Compose 实现:

@Composable
fun NoteScreen(viewModel: NoteViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    LaunchedEffect(Unit) {
        viewModel.events.collect { msg ->
            // 显示 Snackbar
        }
    }

    when (val state = uiState) {
        is NoteUiState.Loading -> CircularProgressIndicator()
        is NoteUiState.Empty -> Text("没有笔记,点 + 添加")
        is NoteUiState.Success -> NoteList(
            notes = state.notes,
            onRefresh = viewModel::refresh,
            onAdd = { title, content -> viewModel.addNote(title, content) }
        )
    }
}

数据流向:

[Repository] -> (Flow) -> [ViewModel] -> (StateFlow) -> [Compose UI]
      ↑                                                    |
      └----------- 用户事件(refresh/addNote)-------------┘

状态从下往上,事件从上往下。这就是单向数据流(UDF)。

Repository 模式

Repository 是 Model 层的核心,负责:

  1. 聚合数据源:网络、数据库、缓存统一管理。
  2. 解决冲突:网络和本地不一致时,决定用哪个。
  3. 缓存策略:先读缓存再更新,或者强制网络。
  4. 业务规则:比如「未登录不允许操作」。

常见模式:离线优先(Offline-First)。数据库是 SSOT,UI 观察数据库。网络拉到新数据先写库,UI 自动收到更新。

class NoteRepository(
    private val dao: NoteDao,
    private val api: NoteApi
) {
    // UI 观察这个 Flow
    fun observeNotes(): Flow<List<Note>> = dao.observeAll()

    // 刷新:先网络,再写库
    suspend fun refresh() {
        val notes = api.listNotes()
        dao.replaceAll(notes)
    }
}

UI 调 refresh(),数据流回来,UI 不用关心网络结果直接用。

Compose 里的状态提升

Compose 配合 MVVM,有个原则叫「状态提升(State Hoisting)」:把状态从子组件移到父组件,子组件变成无状态(Stateless)。

无状态组件只接收数据和回调:

@Composable
fun NoteList(
    notes: List<Note>,
    onRefresh: () -> Unit,
    onAddNote: (String, String) -> Unit
) {
    Column {
        Button(onClick = onRefresh) { Text("刷新") }
        notes.forEach { note ->
            Text(note.title)
        }
    }
}

好处:

  • 组件可复用,状态不同就能用在不同场景。
  • 易测试,传数据进去看渲染。
  • 状态集中在父级(ViewModel),单向数据流清晰。
Note

状态提升不是无脑提,提太高会让中间组件被迫传递数据。原则:「状态给需要它的最近公共祖先」。

UiState 设计

UI 状态怎么设计直接影响代码质量。几个原则:

1. 不可变

data class NoteListUiState(
    val notes: List<Note> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

val,改值用 copy()

2. 用密封类表示互斥状态

sealed interface UiState<out T> {
    object Loading : UiState<Nothing>
    data class Success<T>(val data: T) : UiState<T>
    data class Error(val message: String) : UiState<Nothing>
}

UI 用 when 处理,编译器强制覆盖所有分支。

3. 一次性事件单独流

不要把「显示 Snackbar」这种事件塞进 StateFlow。StateFlow 保留最新值,旋转屏幕会重发。

private val _events = MutableSharedFlow<String>()
val events = _events.asSharedFlow()  // 一次性事件

4. UI 专属字段不要混入数据模型

Note 是数据层模型,UI 要的 isLoadingisEmptyerrorMessage 单独包一层:

data class NoteListUiState(
    val notes: List<Note> = emptyList(),
    val isLoading: Boolean = false
)

不要给 NoteisLoading 字段。

依赖注入

ViewModel 依赖 Repository,Repository 依赖 Dao/Api。怎么把它们组装起来?

手动 DI

最简单的 Factory:

class NoteViewModelFactory(
    private val repo: NoteRepository
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(NoteViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return NoteViewModel(repo) as T
        }
        throw IllegalArgumentException("Unknown ViewModel")
    }
}

// Application 里持有依赖
class App : Application() {
    val noteRepo by lazy {
        NoteRepository(AppDatabase.getInstance(this).noteDao(), RetrofitClient.api)
    }
}

// Activity/Compose 里
val app = context.applicationContext as App
val viewModel = ViewModelProvider(this, NoteViewModelFactory(app.noteRepo))[NoteViewModel::class.java]

Hilt

正式项目用 Hilt,注解自动注入:

@HiltViewModel
class NoteViewModel @Inject constructor(
    private val repo: NoteRepository
) : ViewModel() {
    // ...
}

@Singleton
class NoteRepository @Inject constructor(
    private val dao: NoteDao,
    private val api: NoteApi
) {
    // ...
}

第 97 章会详细讲 Hilt。

测试 MVVM

MVVM 的好处之一是易测试。ViewModel 不依赖 UI,传 Mock 的 Repository 就能测。

class NoteViewModelTest {
    @Test
    fun `refresh success emits success event`() = runTest {
        val repo = mockk<NoteRepository>(relaxed = true)
        val viewModel = NoteViewModel(repo)

        viewModel.refresh()

        // 验证事件
        viewModel.events.test {
            assertEquals("刷新成功", awaitItem())
        }
    }
}

Repository 也能测,Mock Dao 和 Api。UI 层可以用 Compose 测试框架或者 Espresso。

常见反模式

  1. ViewModel 直接持有 View 引用:泄漏,崩。ViewModel 只暴露状态。
  2. Repository 返回 LiveData:LiveData 是 UI 层的东西。Repository 用 Flow。
  3. UI 直接调 Repository:跳过 ViewModel,业务逻辑散到 UI。所有数据操作走 ViewModel。
  4. StateFlow 装一次性事件:旋转屏幕重复触发。用 SharedFlow。
  5. MutableState 满天飞:状态分散,难追踪。集中到 UiState。
  6. UI 状态字段混入数据模型:耦合。数据层和 UI 层分开。

常见坑

  1. ViewModel 不重新创建:跨进程恢复时实例丢失,要 SavedStateHandle。
  2. SharedFlow 默认 replay=0:晚订阅的事件收不到。需要历史事件设 replay。
  3. collectAsState 没 WithLifecycle:后台还在收集,浪费电。
  4. Compose 重组触发副作用:副作用要放 LaunchedEffect,别直接写在 Composable 里。
  5. Repository 不缓存实例:每次新建,依赖状态丢失。用单例或者 DI。

小结

MVVM 三层:Model(Repository + DataSource)、ViewModel(状态 + 逻辑)、View(Compose)。Repository 是数据汇聚点,ViewModel 暴露不可变 StateFlow + SharedFlow 事件,View 观察状态发事件。Compose 配合状态提升,子组件无状态可复用。手写 Factory 简单,正式项目用 Hilt。

下一章讲 Jetpack 全景,看这套生态里还有哪些库能帮你写应用。