从 0 开始学 Jetpack Compose|第八篇:ViewModel + Compose 架构实战

0 阅读4分钟

👋 哈喽大家好,欢迎回到 Compose 零基础系列~前面七篇我们已经把组件、布局、样式、状态、列表、主题、页面跳转全都学完了,写个简单的多页面 APP 已经完全没问题。

但不知道你有没有发现一个问题:我们之前写的代码,UI 和数据逻辑混在一起。页面一多、逻辑一复杂,代码就会变得又乱又难维护,而且状态还容易丢失、内存泄漏。

在正式项目里,我们绝对不会这么写。Google 官方推荐的最佳实践是:Compose + ViewModel

这一篇,我就用最通俗、最接地气的方式,带你真正进入项目级架构。不讲虚的理论,全程实战、代码可直接复制,学完你的代码风格会直接提升一个档次。

📌 本篇核心目标

  • 搞懂 ViewModel 到底是干嘛的
  • 学会为什么要用 ViewModel,而不是只用 remember
  • 掌握 ViewModel 最基本用法:保存状态、分离逻辑
  • 学会在 Compose 中使用 StateFlow / SharedFlow
  • 实现网络请求模拟、列表加载、分页、加载状态
  • 写出规范、可维护、接近企业级的代码结构

一、先说人话:ViewModel 到底有啥用?

你可以把它理解成:页面的 “数据管家”

它主要帮你干 4 件事:

  1. 状态保存屏幕旋转、语言切换、系统杀进程恢复时,remember 会丢数据,但 ViewModel 不会。
  2. UI 与逻辑分离页面只负责画 UI,所有业务逻辑、数据请求都丢给 ViewModel。
  3. 数据共享多个页面、多个组件可以共用同一个 ViewModel 的数据。
  4. 生命周期安全不会内存泄漏,生命周期比页面长,页面销毁它自动清理。

一句话总结:ViewModel = 存放数据 + 处理逻辑 + 保状态 + 防泄漏


二、第一步:添加依赖

app/build.gradle 里加上:

kotlin

// ViewModel Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")

// 协程
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

同步一下即可。


三、最简单示例:ViewModel 保存计数器

我们先写一个最经典的计数器,对比 remember 和 ViewModel 的区别。

3.1 创建 ViewModel

kotlin

class CountViewModel : ViewModel() {
    // 定义状态
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count

    // 加一
    fun addCount() {
        _count.value++
    }
}

3.2 在 Compose 中使用

kotlin

@Composable
fun CountScreen(viewModel: CountViewModel = viewModel()) {
    // 收集状态
    val count by viewModel.count.collectAsStateWithLifecycle()

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text("当前计数:$count", fontSize = 24.sp)

        Button(onClick = { viewModel.addCount() }) {
            Text("点击 +1")
        }
    }
}

重点:

  • viewModel() 系统自动创建、管理生命周期
  • collectAsStateWithLifecycle() 只有页面可见时才更新,省电、安全

你旋转屏幕,数字不会重置,这就是 ViewModel 的作用。


四、实战:完整列表 + 加载状态 + 模拟网络

这才是真实项目常用结构:

  • 加载中
  • 加载成功(展示列表)
  • 加载失败
  • 下拉刷新 / 点击重试

4.1 定义数据类

kotlin

data class Article(
    val id: Int,
    val title: String,
    val desc: String
)

4.2 定义状态密封类(规范写法)

kotlin

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

4.3 写 ViewModel(核心)

kotlin

class ListViewModel : ViewModel() {

    private val _uiState = MutableStateFlow<UiState<List<Article>>>(UiState.Loading)
    val uiState: StateFlow<UiState<List<Article>>> = _uiState

    init {
        loadData()
    }

    // 模拟网络请求
    fun loadData() {
        _uiState.value = UiState.Loading

        viewModelScope.launch {
            // 模拟延迟
            delay(1000)

            // 模拟数据
            val data = List(10) {
                Article(
                    id = it,
                    title = "文章标题 $it",
                    desc = "这是文章的详细描述内容 $it"
                )
            }

            _uiState.value = UiState.Success(data)
        }
    }
}

4.4 UI 页面根据状态显示不同内容

kotlin

@Composable
fun ListScreen(viewModel: ListViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(
            text = "文章列表",
            style = MaterialTheme.typography.headlineSmall,
            modifier = Modifier.padding(bottom = 16.dp)
        )

        when (uiState) {
            is UiState.Loading -> {
                Box(Modifier.fillMaxSize()) {
                    CircularProgressIndicator(Modifier.align(Alignment.Center))
                }
            }

            is UiState.Success<*> -> {
                val list = (uiState as UiState.Success<List<Article>>).data

                LazyColumn(
                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    items(list) { article ->
                        ArticleItem(article)
                    }
                }
            }

            is UiState.Error -> {
                val msg = (uiState as UiState.Error).msg
                Box(Modifier.fillMaxSize()) {
                    Text(
                        text = "加载失败:$msg",
                        modifier = Modifier.align(Alignment.Center),
                        color = Color.Red
                    )
                }
            }
        }
    }
}

@Composable
fun ArticleItem(article: Article) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.medium)
            .padding(16.dp)
    ) {
        Text(
            text = article.title,
            style = MaterialTheme.typography.titleLarge
        )
        Text(
            text = article.desc,
            style = MaterialTheme.typography.bodyMedium,
            modifier = Modifier.padding(top = 4.dp)
        )
    }
}

运行效果:

  • 进入页面先显示加载圈
  • 1 秒后展示列表
  • 结构清晰、UI 和逻辑完全分离

五、为什么要用 StateFlow?为什么不用 remember?

很多新手会问:我用 remember 也能存状态,为啥要搞这么复杂?

我给你说真实开发理由:

  1. remember 只在当前 Composable 有效跳别的页面再回来,状态会重置。
  2. ViewModel 生命周期比 Compose 长旋转屏幕、切换夜间模式,数据不会丢。
  3. 逻辑可以复用、可测试网络请求、数据库操作写在 ViewModel,不跟 UI 耦合。
  4. 避免内存泄漏viewModelScope 自动跟着销毁,不会泄漏。

六、新手最容易踩的坑

  1. 把网络请求写在 @Composable 里每次重组都会发请求,疯狂重复调用。
  2. 直接用 mutableStateOf 代替 StateFlow小 demo 可以,项目不行,无法生命周期安全。
  3. 不使用 collectAsStateWithLifecycle后台也更新 UI,耗电、卡顿、崩溃风险。
  4. 一个 ViewModel 塞太多东西一个页面一个 ViewModel,不要全局大一统。
  5. 忘记处理 Loading / Error用户体验极差,正式项目必做状态封装。

七、本篇总结 + 下篇预告

本篇收获

  • 理解 ViewModel = 数据管家,负责存状态、处理逻辑
  • 学会 StateFlow + 状态收集
  • 学会规范的 UiState 封装(加载 / 成功 / 失败)
  • 写出了真正接近企业级的列表架构
  • 代码结构清晰、可维护、可扩展

下篇预告

第九篇:Compose 动画基础与常用交互包括:

  • 淡入淡出
  • 位移动画
  • 尺寸动画
  • 点击涟漪、列表动画
  • 简单炫酷的交互动画

八、系列更新进度

  1. 第一篇:零基础入门,环境搭建 + 第一个页面
  2. 第二篇:基础组件 + 三大布局
  3. 第三篇:Modifier 修饰符全解
  4. 第四篇:状态管理 remember、mutableStateOf
  5. 第五篇:LazyColumn、LazyRow 列表
  6. 第六篇:Material3 主题与深色模式
  7. 第七篇:Navigation 页面跳转与传参
  8. 第八篇:ViewModel + Compose 架构实战(当前)
  9. 第九篇:Compose 动画基础
  10. 第十篇:综合实战项目