3. 第一个协程
在 Android 里,协程总是挂在一个 CoroutineScope 上启动。最常见的是 lifecycleScope(Activity/Fragment)和 viewModelScope(ViewModel)。下面是四种实际位置:
① Activity / Fragment:lifecycleScope
按钮点击后启动后台任务,页面销毁时自动取消。代码写在 Activity 的 onCreate 或 Fragment 的 onViewCreated 里。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btnLoad).setOnClickListener {
lifecycleScope.launch { // 与 Activity 生命周期绑定
delay(1000L) // 挂起 1 秒,不卡主线程
Toast.makeText(
this@MainActivity,
"加载完成",
Toast.LENGTH_SHORT
).show()
}
}
}
}
② ViewModel:viewModelScope
把数据请求放进 ViewModel,屏幕旋转等配置变更不会泄漏,ViewModel 被清除时自动取消。代码写在 ViewModel 的方法里。
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow<User?>(null)
val uiState: StateFlow<User?> = _uiState.asStateFlow()
fun loadUser() {
viewModelScope.launch { // ViewModel 被清空时自动 cancel
val user = repository.getUser("1") // 挂起函数
_uiState.value = user
}
}
}
③ Repository / UseCase:coroutineScope 组织并发
如果你一直用传统 XML 开发,可能习惯把网络请求、数据库读写直接写在 Activity 里(AsyncTask、Handler、runOnUiThread 那一套)。现代 Android 推荐把代码分层,让每一层只干一件事。下面这张表把现代分层和你原来的写法对照起来:
| 层 | 职责 | 传统 XML 写法的对应 |
|---|---|---|
| UI 层 (Activity / Fragment) | 只负责展示界面、接收用户点击,不碰数据逻辑 | 你原来写 AsyncTask 内部类、在 onPostExecute 里更新 UI 的那一层 |
| ViewModel 层 | 持有 UI 状态、调用业务;屏幕旋转不丢数据 | 基本没有 —— 原来数据都临时挂在 Activity 字段上,旋转就丢 |
| Repository 层 | 统一对外提供数据,屏蔽「数据来自网络 / 数据库 / 缓存」 | 原来散落在 Activity 里的 retrofit 调用、SQLite 代码 |
| UseCase 层 (可选) | 一个类只封装「一个业务动作」,把多个 Repository 调用组合成有意义的操作 | 原来写在 Activity 里的一长串业务逻辑方法 |
Repository 是什么?—— 数据仓库
简单说:Repository 就是「数据仓库」。UI 层只管问它要数据,至于数据是从 Retrofit 接口拉、从 Room 数据库读、还是从内存缓存拿,UI 层完全不关心。好处是:哪天你把网络接口换成本地数据库,Activity 一行都不用改。代码写在独立的 XXXRepository 类里。
// 数据接口:数据具体从哪来,由实现类决定
interface UserApi {
suspend fun getProfile(): Profile
suspend fun getOrders(): List<Order>
}
// Repository:把「网络细节」藏起来,对外只暴露一个挂起函数
// 用 coroutineScope 同时发起两个请求,都结束后才汇总返回
class UserRepository(private val api: UserApi) {
suspend fun fetchDashboard(): Dashboard = coroutineScope {
val profile = async { api.getProfile() } // 并行开始
val orders = async { api.getOrders() } // 并行开始
Dashboard(profile.await(), orders.await()) // 两个都完成才汇总
}
}
UseCase 是什么?—— 一个业务动作
UseCase 是「一个业务动作」的封装。当某个操作要在多处复用、或需要把多个 Repository 串起来(比如「登录后拉用户资料、再拉配置」),就抽成一个 UseCase 类。它只写业务规则,不碰线程、不碰 UI,内部用 coroutineScope 组织并发。代码写在 XXXUseCase 类里。
// UseCase:封装「进入首页」这个业务动作,组合两个 Repository
class LoadHomeUseCase(
private val userRepo: UserRepository,
private val configRepo: ConfigRepository
) {
// 返回首页所需全部数据;内部并行拉取,任一个失败则整体抛异常
suspend operator fun invoke(): HomeData = coroutineScope {
val dashboard = async { userRepo.fetchDashboard() }
val config = async { configRepo.fetchConfig() }
HomeData(dashboard.await(), config.await())
}
}
// ViewModel 里这样用:一行调用,业务细节全藏在 UseCase 里
class HomeViewModel(private val loadHome: LoadHomeUseCase) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun onEnter() {
viewModelScope.launch {
_uiState.value = try {
UiState.Success(loadHome()) // invoke() 是挂起函数,自动在协程里跑
} catch (e: Exception) {
UiState.Error(e.message ?: "加载失败")
}
}
}
}
不是每个功能都要写 UseCase。单接口、一次请求的直接在 ViewModel 调 Repository 即可;只有当一段业务逻辑要在多处复用、或需要组合多个数据源时,才值得抽成 UseCase。Repository 才是几乎每个项目都该有的层。
对比一下你原来的写法:传统 XML 里,你可能会在 Activity 里 new 一个 AsyncTask,在 doInBackground 调接口、在 onPostExecute 更新 TextView,还要判空、判 Activity 是否还活着。现在这套逻辑被拆成:Activity 只调 viewModel.onEnter() → ViewModel 用协程调用 UseCase → UseCase 组合 Repository → Repository 用 withContext(Dispatchers.IO) 切线程。每一层都能单独测试,且不依赖 Activity 生命周期。
④ 测试或 main 函数:runBlocking
仅在单元测试或命令行入口里用来桥接普通函数与挂起函数。不要写在 Android 主线程业务代码中。
@Test
fun testUser() = runBlocking {
val repo = UserRepository(fakeApi)
val dashboard = repo.fetchDashboard()
assertNotNull(dashboard)
}
生产环境别用 GlobalScope:GlobalScope.launch 生命周期跟随整个应用,不受 Activity/Fragment/ViewModel 管理,极易造成内存泄漏,Android 业务代码中应避免。