一句话总结:
不要让你的 Activity(前台服务员)亲自去“点外卖”和“等外卖”。正确的做法是让 ViewModel(餐厅经理)在后台通过协程(最高效的订餐系统)去处理,然后将做好的“菜品”(LiveData/StateFlow)直接送到前台,服务员只管上菜(更新 UI)即可。
第一篇章:“经典”但危险的模式——在 Activity 中直接请求
你的文章已经出色地演示了这种经典模式。它直观、易于理解,但背后隐藏着三大风险:
- 屏幕旋转后数据丢失/App 崩溃。
- 退出页面后网络请求仍在继续,造成内存泄漏和资源浪费。
- 所有逻辑耦合在
Activity中,代码难以测试和维护。
这种模式适用于快速原型验证,但严禁用于生产环境。
第二篇章:现代化的“三层架构”——解耦你的网络请求
一个健壮的网络请求,应该被清晰地划分为三层。
1. 数据层 (Repository):真正的“外卖员”
创建一个 Repository 类,专门负责所有的数据获取逻辑。它应该使用 Kotlin 协程的 suspend 函数 来封装 OkHttp 的调用,将回调地狱转换为干净的同步代码。
Kotlin
// OkHttp + Coroutine 的扩展函数,将回调转换为 suspend 函数
suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response)
}
override fun onFailure(call: Call, e: IOException) {
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
})
continuation.invokeOnCancellation {
cancel()
}
}
}
// 数据仓库
class PostRepository {
private val client = OkHttpClient()
private val gson = Gson()
// 这是一个挂起函数,它必须在协程中被调用
suspend fun getPost(): Post {
val request = Request.Builder()
.url("https://jsonplaceholder.typicode.com/posts/1")
.build()
// 在 IO 线程中执行网络请求
return withContext(Dispatchers.IO) {
val response = client.newCall(request).await()
val json = response.body?.string()
gson.fromJson(json, Post::class.java)
}
}
}
2. 业务逻辑层 (ViewModel):“餐厅经理”
ViewModel 是这场变革的核心。它能在屏幕旋转后存活,是处理业务逻辑和持有 UI 状态的完美场所。
Kotlin
class MainViewModel : ViewModel() {
private val repository = PostRepository()
// 使用 LiveData 向 UI 暴露数据。LiveData 是生命周期感知的。
private val _post = MutableLiveData<Post>()
val post: LiveData<Post> = _post
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
fun fetchPost() {
// viewModelScope 保证协程在 ViewModel 销毁时自动取消,杜绝内存泄漏
viewModelScope.launch {
try {
_post.value = repository.getPost()
} catch (e: Exception) {
_error.value = "请求失败: ${e.message}"
}
}
}
}
3. 表现层 (Activity):只负责展示的“服务员”
Activity 的代码变得极其简单和干净。它不再关心网络请求如何发起、在哪个线程执行,只负责观察 ViewModel 中的数据并更新 UI。
Kotlin
class MainActivity : AppCompatActivity() {
private lateinit var textView: TextView
// 使用 by viewModels() 委托属性来获取 ViewModel 实例
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.text_view)
// 观察 post 数据的变化
viewModel.post.observe(this) { post ->
textView.text = post.title
}
// 观察错误信息的变化
viewModel.error.observe(this) { errorMessage ->
Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()
}
// 触发数据加载
viewModel.fetchPost()
}
// 注意:不再需要 onDestroy 中手动取消网络请求!
}
三、新旧模式的终极对比
| 维度 | 旧模式 (Activity 中直接请求) | 现代 MVVM 架构 |
|---|---|---|
| 生命周期管理 | 手动在 onDestroy 中取消,极易遗忘,导致泄漏 | 自动,viewModelScope 与 ViewModel 生命周期绑定 |
| 配置变更 | 数据丢失,甚至崩溃 | ViewModel 存活,数据无缝恢复 |
| 线程切换 | 手动调用 runOnUiThread | 自动,LiveData/Flow 自动将结果分发到主线程 |
| 代码结构 | 高度耦合,所有逻辑混在 Activity | 高度解耦,职责清晰 (Repository -> ViewModel -> Activity) |
| 可测试性 | 极差,无法对 Activity 进行单元测试 | 极佳,可以轻松地对 Repository 和 ViewModel 进行单元测试 |
最终结论:
使用 OkHttp 发起异步请求,技术实现本身非常简单。但构建一个能在真实、复杂场景下(如屏幕旋转、频繁进出页面)稳定工作的网络请求功能,则必须超越“如何调用 API”的层面,转向现代化的、生命周期安全的架构设计。