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,根据不同的状态渲染不同的界面。
实战:用户信息加载
好,咱们动手写一个完整的例子。假设我们要加载一个用户的信息,包含头像、昵称、简介。
先定义数据模型:
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)
}
}
流程图:LiveData + 网络请求的完整生命周期
下面这张图,我画了从用户点击到界面更新的完整流程。你看一遍就能理解为什么ViewModel和LiveData是天生一对。
避坑指南:我曾经踩过的三个坑
- 坑一:忘记处理Loading状态。 我早期写代码,只处理了Success和Error。结果用户点加载后,界面卡住不动,过了两秒才刷新。用户体验极差。后来我强制自己:每次请求必须先发Loading。
- 坑二:在Activity里直接new ViewModel。 这样写,屏幕旋转后ViewModel就丢了。必须用
by viewModels()或者ViewModelProvider。 - 坑三:多个LiveData分别管理状态。 有人用三个LiveData:
loadingLiveData、successLiveData、errorLiveData。结果状态同步出了问题——Loading和Success同时为true。用密封类+一个LiveData,完美解决。
总结一下
LiveData和网络请求结合,说白了就是三句话:
- ViewModel里发起请求,用viewModelScope管理协程生命周期。
- 用密封类定义三种状态:Loading、Success、Error。
- UI层只观察一个LiveData,根据状态渲染不同界面。
这套模式,我在好几个项目里用过,稳定、清晰、好维护。你照着写,基本不会出大问题。
公众号:蓝海资料掘金营,微信deep3321