协程速通 · Android

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 业务代码中应避免。

Kotlin 协程速通 · Android 开发 · 内容基于 kotlinx.coroutines 1.7+