7. LiveData与网络请求结合:在ViewModel中发起网络请求、使用LiveData管理请求状态(Loading/Success/Error)、实战:用户信息加载

好,咱们今天聊点实战的。前面几章我们把LiveData和ViewModel的基本用法过了一遍,但说实话,那些都是「热身」。真正到了项目里,你面对的第一个硬骨头就是——网络请求

我刚开始用MVVM架构时,犯过一个低级错误:直接在Activity里写网络回调,结果屏幕旋转一下,请求结果丢了,界面还崩了。嗯,从那以后我学乖了——网络请求必须交给ViewModel,状态必须用LiveData管起来

为什么非得在ViewModel里发起网络请求?

你想想看,网络请求是异步的。用户点了个按钮,请求发出去,这时候如果手机转了个方向,Activity重建了——如果请求写在Activity里,回调就丢了,甚至可能内存泄漏。

ViewModel就不一样。它生命周期比Activity长,能扛住配置变更。请求发出去,Activity销毁重建,ViewModel还在,回调照常执行。LiveData再把结果推给新的Activity,界面自动更新。

说白了,ViewModel管「干活」,LiveData管「通知」

用LiveData管理请求状态:Loading / Success / Error

一个网络请求,无非三种状态:加载中、成功、失败。很多新手只用一个LiveData存结果,成功了就赋值,失败了就抛异常。这样写,UI层根本不知道什么时候该显示加载动画。

我个人习惯定义一个密封类,把三种状态包起来:

sealed class Result<out T> {
    data class Loading<T>(val data: T? = null) : Result<T>()
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Throwable, val message: String? = null) : Result<Nothing>()
}

然后在ViewModel里,用一个LiveData<Result<User>>来暴露状态。UI层只需要观察这个LiveData,根据不同的状态渲染不同的界面。

核心思路: 一个LiveData,三种状态。UI层只关心「当前是什么状态」,不关心「请求是怎么发的」。

实战:用户信息加载

好,咱们动手写一个完整的例子。假设我们要加载一个用户的信息,包含头像、昵称、简介。

先定义数据模型:

data class User(
    val id: Long,
    val avatarUrl: String,
    val nickname: String,
    val bio: String
)

再写一个假的网络仓库(实际项目里换成Retrofit就行):

class UserRepository {
    suspend fun fetchUser(userId: Long): User {
        // 模拟网络延迟
        delay(1500)
        // 模拟成功
        return User(
            id = userId,
            avatarUrl = "https://example.com/avatar/$userId",
            nickname = "用户$userId",
            bio = "这个人很懒,什么都没写"
        )
        // 模拟失败可以抛异常
        // throw IOException("网络连接失败")
    }
}

接下来是ViewModel的核心逻辑:

class UserViewModel : ViewModel() {
    private val repository = UserRepository()

    private val _userResult = MutableLiveData<Result<User>>()
    val userResult: LiveData<Result<User>> get() = _userResult

    fun loadUser(userId: Long) {
        // 1. 先发Loading状态
        _userResult.value = Result.Loading()

        // 2. 用viewModelScope发起协程
        viewModelScope.launch {
            try {
                val user = repository.fetchUser(userId)
                // 3. 成功
                _userResult.value = Result.Success(user)
            } catch (e: Exception) {
                // 4. 失败
                _userResult.value = Result.Error(e, e.message ?: "未知错误")
            }
        }
    }
}

这里有个细节:viewModelScope是ViewModel自带的协程作用域。当ViewModel被清除时,它会自动取消所有协程,不会泄漏。我曾经见过有人用GlobalScope发请求,结果页面关了还在跑,白白浪费流量。

最后是UI层的观察:

class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)

        viewModel.userResult.observe(this) { result ->
            when (result) {
                is Result.Loading -> {
                    // 显示加载动画
                    binding.progressBar.visibility = View.VISIBLE
                    binding.errorView.visibility = View.GONE
                    binding.userView.visibility = View.GONE
                }
                is Result.Success -> {
                    // 隐藏加载动画,显示用户信息
                    binding.progressBar.visibility = View.GONE
                    binding.userView.visibility = View.VISIBLE
                    binding.userView.bind(result.data)
                }
                is Result.Error -> {
                    // 隐藏加载动画,显示错误提示
                    binding.progressBar.visibility = View.GONE
                    binding.errorView.visibility = View.VISIBLE
                    binding.errorView.text = result.message
                }
            }
        }

        // 触发加载
        viewModel.loadUser(1001L)
    }
}
小技巧: 如果你用DataBinding,可以把Result状态直接绑定到布局上,连when表达式都省了。不过初学者建议先写when,逻辑更清晰。

流程图:LiveData + 网络请求的完整生命周期

下面这张图,我画了从用户点击到界面更新的完整流程。你看一遍就能理解为什么ViewModel和LiveData是天生一对。

LiveData + 网络请求生命周期 用户点击加载 ViewModel.loadUser() 设置 LiveData 为 Loading 状态 viewModelScope.launch { } 协程内执行网络请求 Result.Success(user) LiveData 推送成功数据 Result.Error(e) LiveData 推送错误信息 UI 层自动更新
注意: 不要在ViewModel里持有Activity或View的引用。网络请求的回调里如果直接操作UI,一旦Activity销毁,就会空指针。用LiveData就是为了解耦——ViewModel只负责发数据,不负责谁在收。

避坑指南:我曾经踩过的三个坑

  • 坑一:忘记处理Loading状态。 我早期写代码,只处理了Success和Error。结果用户点加载后,界面卡住不动,过了两秒才刷新。用户体验极差。后来我强制自己:每次请求必须先发Loading
  • 坑二:在Activity里直接new ViewModel。 这样写,屏幕旋转后ViewModel就丢了。必须用by viewModels()或者ViewModelProvider
  • 坑三:多个LiveData分别管理状态。 有人用三个LiveData:loadingLiveDatasuccessLiveDataerrorLiveData。结果状态同步出了问题——Loading和Success同时为true。用密封类+一个LiveData,完美解决。

总结一下

LiveData和网络请求结合,说白了就是三句话:

  1. ViewModel里发起请求,用viewModelScope管理协程生命周期。
  2. 用密封类定义三种状态:Loading、Success、Error。
  3. UI层只观察一个LiveData,根据状态渲染不同界面。

这套模式,我在好几个项目里用过,稳定、清晰、好维护。你照着写,基本不会出大问题。

核心心法: ViewModel是「大脑」,LiveData是「嘴巴」。大脑思考(发请求),嘴巴说话(通知UI)。别让大脑直接去碰屏幕。

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