28、ViewModel + RecyclerView:结合 MVVM 架构管理列表数据

说实话,前面几章我们把 RecyclerView 的用法讲得差不多了。但有个问题一直悬着——数据到底该放哪?

我早期做项目时,习惯把列表数据直接塞在 Fragment 或 Activity 里。结果呢?屏幕一旋转,数据全没了。要么就得靠 onSaveInstanceState 手动存,麻烦得要命。后来接触了 MVVM 架构,才意识到:数据的管理,应该交给 ViewModel

这一章,我们就来聊聊怎么把 ViewModel 和 RecyclerView 结合起来。说白了,就是让列表数据有个靠谱的“家”。

为什么需要 ViewModel?

先问个问题:你的列表数据,真的需要 ViewModel 吗?

如果你的列表只是展示几个静态字符串,那确实没必要。但真实项目里,列表数据往往来自网络请求、本地数据库,或者需要用户交互修改。这时候,ViewModel 的价值就体现出来了。

  • 生命周期感知:ViewModel 在 Activity/Fragment 重建时自动保留数据
  • 数据与视图分离:列表数据不再依赖 UI 组件,方便测试
  • 配合 LiveData/Flow:数据变化时自动通知 RecyclerView 刷新

我在项目中遇到过最典型的场景:用户在一个列表页筛选条件,切到别的 App 再回来,筛选状态全丢了。用 ViewModel 之后,这个问题再没出现过。

MVVM 架构下的列表数据流

咱们先画个图,看看 ViewModel 在列表数据流里到底扮演什么角色。

MVVM + RecyclerView 数据流 View 层 Activity / Fragment 持有 RecyclerView 观察 ViewModel 层 持有 LiveData<List<T>> 管理列表数据 调用 数据源 网络/数据库 数据回调 RecyclerView 自动刷新(通过 Adapter.submitList()) 数据变化

这个图你看懂了吗?核心就一句话:ViewModel 持有数据,View 观察数据,数据变了 RecyclerView 自动刷新

实战:写一个 ViewModel 管理列表

光说不练假把式。咱们直接写代码。

假设我们要做一个待办事项列表。每个事项有标题、描述和完成状态。

1. 定义数据模型

data class TodoItem(
    val id: Long,
    val title: String,
    val description: String,
    val isCompleted: Boolean = false
)

2. 创建 ViewModel

class TodoViewModel : ViewModel() {

    // 使用 MutableLiveData 管理列表
    private val _todoList = MutableLiveData<List<TodoItem>>()
    val todoList: LiveData<List<TodoItem>> = _todoList

    // 初始化一些数据
    init {
        loadTodos()
    }

    private fun loadTodos() {
        // 模拟从网络或数据库加载
        val items = listOf(
            TodoItem(1, "学习 RecyclerView", "掌握基础用法", false),
            TodoItem(2, "学习 ViewModel", "理解生命周期", true),
            TodoItem(3, "完成项目", "结合 MVVM 实战", false)
        )
        _todoList.value = items
    }

    // 添加新事项
    fun addTodo(title: String, description: String) {
        val currentList = _todoList.value?.toMutableList() ?: mutableListOf()
        val newId = (currentList.maxOfOrNull { it.id } ?: 0) + 1
        currentList.add(TodoItem(newId, title, description))
        _todoList.value = currentList
    }

    // 切换完成状态
    fun toggleCompleted(id: Long) {
        val currentList = _todoList.value?.toMutableList() ?: return
        val index = currentList.indexOfFirst { it.id == id }
        if (index != -1) {
            currentList[index] = currentList[index].copy(
                isCompleted = !currentList[index].isCompleted
            )
            _todoList.value = currentList
        }
    }
}
💡 我的习惯:用 _todoList 作为私有 MutableLiveData,对外暴露不可变的 LiveData。这样外部只能观察,不能修改,数据流向更清晰。

3. 在 Fragment 中绑定 ViewModel 和 RecyclerView

class TodoListFragment : Fragment() {

    private lateinit var binding: FragmentTodoListBinding
    private lateinit var adapter: TodoAdapter
    private val viewModel: TodoViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentTodoListBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        setupRecyclerView()
        observeViewModel()
    }

    private fun setupRecyclerView() {
        adapter = TodoAdapter { todoItem ->
            // 点击切换完成状态
            viewModel.toggleCompleted(todoItem.id)
        }
        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(requireContext())
            adapter = this@TodoListFragment.adapter
        }
    }

    private fun observeViewModel() {
        viewModel.todoList.observe(viewLifecycleOwner) { items ->
            // 数据变化时,自动刷新列表
            adapter.submitList(items)
        }
    }
}

你看,Fragment 的代码变得非常干净。它只负责两件事:设置 RecyclerView,观察 ViewModel 的数据变化。至于数据怎么来、怎么改,那是 ViewModel 的事。

避坑指南:我踩过的几个坑

⚠️ 我曾经犯过的错:
  • 直接修改 LiveData 的 value:比如 _todoList.value?.add(item),这样不会触发数据更新。必须重新赋值一个新的 List 对象。
  • 在 ViewModel 里持有 Context:ViewModel 生命周期比 Activity 长,持有 Context 会导致内存泄漏。用 AndroidViewModel 时要小心。
  • 忘记使用 viewLifecycleOwner:在 Fragment 里观察 LiveData 时,一定要用 viewLifecycleOwner,而不是 this。否则可能引发多次观察。

进阶:使用 StateFlow 替代 LiveData

如果你用的是 Kotlin 协程,我个人更推荐用 StateFlow。它和 LiveData 功能类似,但更灵活。

class TodoViewModel : ViewModel() {

    private val _todoList = MutableStateFlow<List<TodoItem>>(emptyList())
    val todoList: StateFlow<List<TodoItem>> = _todoList.asStateFlow()

    init {
        loadTodos()
    }

    private fun loadTodos() {
        // 模拟加载
        _todoList.value = listOf(
            TodoItem(1, "学习 RecyclerView", "掌握基础用法", false),
            TodoItem(2, "学习 ViewModel", "理解生命周期", true)
        )
    }

    fun addTodo(title: String, description: String) {
        val currentList = _todoList.value.toMutableList()
        currentList.add(TodoItem(
            id = (currentList.maxOfOrNull { it.id } ?: 0) + 1,
            title = title,
            description = description
        ))
        _todoList.value = currentList
    }
}

在 Fragment 里观察 StateFlow 也很简单:

lifecycleScope.launch {
    viewModel.todoList.collect { items ->
        adapter.submitList(items)
    }
}

核心要点总结:

  • ViewModel 负责管理列表数据,不依赖 UI 组件
  • 通过 LiveData 或 StateFlow 将数据暴露给 View 层
  • View 层只做观察和展示,不直接操作数据
  • 数据变化时,调用 adapter.submitList() 自动刷新
  • 注意 LiveData 的赋值方式,必须重新创建 List 对象

嗯,这一章的内容就这些。ViewModel + RecyclerView 的组合,说白了就是让数据管理变得有章可循。你想想看,以前写列表时那些乱七八糟的状态管理,现在是不是清晰多了?

下一章我们会聊聊更高级的话题——分页加载。到时候你会看到,ViewModel 在管理大量列表数据时,能发挥更大的作用。


公众号:蓝海资料掘金营,微信deep3321