协程环境搭建:从依赖引入到 Scope 创建

做协程开发,第一步不是写代码,是把架子搭好。我见过不少同学在项目里直接 copy 网上代码,结果编译报错半天找不到原因——说白了,就是依赖没配对。

这一章,我们先把基础环境铺平。我会带你从头创建一个 Kotlin 项目,然后引入协程核心库,最后创建一个 CoroutineScope。相信我,这一步如果偷懒,后面排查启动时序问题时会多花十倍的时间。

2.1 新建 Kotlin 项目并添加协程依赖

我用的是 IntelliJ IDEA,创建一个普通的 Kotlin/JVM 项目就行。如果你是用 Android Studio,操作也差不多。这里我假设你已经有了一个项目,我们直接看 build.gradle.kts 里的依赖配置。

核心依赖:org.jetbrains.kotlinx:kotlinx-coroutines-core

打开你的 build.gradle.kts(或 build.gradle),在 dependencies 块里加入以下几行:

dependencies {
    // Kotlin 标准库(通常已默认)
    implementation("org.jetbrains.kotlin:kotlin-stdlib")

    // 协程核心库
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")

    // 如果你用 Android,还需要添加这个(后面章节会细讲)
    // implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}

为什么只加 core 就够?因为协程的核心 API——比如 launch、async、CoroutineScope——都在这个包里。Android 版多了一些调度器和生命周期绑定,我们现在先不管。

我个人习惯在项目里统一管理协程版本号,比如在 gradle.properties 里定义:kotlinCoroutinesVersion=1.8.1,然后在 dependencies 里引用 ${kotlinCoroutinesVersion}。这样升级版本时改一个地方就行。

添加完后,记得点一下「Sync Now」或者执行 ./gradlew build。等一下……嗯,我第一次做这一步时卡在了版本不兼容上。后来发现 Kotlin 1.9.x 对应协程 1.7.x 以上基本都兼容,但如果你的 Kotlin 版本特别老,建议先升级 Kotlin。

2.2 理解 CoroutineScope:它到底是个什么东西?

你可以把 CoroutineScope 想象成一个「协程容器」。每个协程都必须生活在某个 scope 里。scope 负责追踪所有它启动的协程,当 scope 被取消时,内部所有协程都会被自动取消。

来看一个简单例子:

// 创建一个空的协程作用域
val myScope = CoroutineScope(Dispatchers.Default)

这里 Dispatchers.Default 是调度器,告诉协程跑在哪个线程池。后面我们专门讲调度器,现在你只要记住:scope 创建时必须指定一个调度器(或使用 SupervisorJob() 组合)。

为什么不是直接用 GlobalScope?我曾经在早期项目里图省事,直接用 GlobalScope.launch,结果 activity 销毁了协程还在跑,内存泄漏得一塌糊涂。所以 根本不建议在业务代码里使用 GlobalScope,它没有生命周期管理,说白了就是个全局静态 scope,你几乎用不到它。

Scope 类型生命周期使用场景
GlobalScope应用进程级别极少使用,仅限临时工具类或测试
MainScope绑定到 UI 组件(如 Activity)Android 的 UI 层(现在更推荐 lifecycleScope)
自定义 CoroutineScope由开发者手动控制大部分业务场景,灵活可控

千万不要轻易用 GlobalScope。我见过有人把它当默认选项,结果协程失控。记住:scope 的生命周期应当和它的使用者一致。

2.3 创建自定义 CoroutineScope 的最佳实践

我通常会在业务类里直接声明一个 scope 属性,并在不需要时主动取消它。比如:

class MyRepository {
    // 使用 SupervisorJob + Dispatchers.IO 组合
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun loadData() {
        scope.launch {
            // 在这里进行 IO 操作
            delay(1000)
            println("数据加载完成")
        }
    }

    // 在 Repository 不再使用时调用
    fun destroy() {
        scope.cancel()
    }
}

注意我用了 SupervisorJob() 而不是 Job()。区别在于:如果 launch 里某个子协程抛异常,SupervisorJob 不会把这个异常向上传播导致整个 scope 取消。这在多个独立子任务并行时非常有用。你想想看,如果下载图片的子协程因为网络问题抛了个异常,结果把整个 Repository 里的数据加载协程都取消了——多冤啊。

2.4 时序问题从 scope 创建就开始了

为什么环境搭建跟启动时序有关?因为 scope 创建的时机和生命周期直接影响协程何时启动、何时取消。举个例子:

class MyViewModel {
    private var scope: CoroutineScope? = null

    fun onStart() {
        // 延迟创建:假设 onStart 在某个回调里被触发
        scope = CoroutineScope(Dispatchers.Main)
        scope?.launch {
            // 如果在这里访问了尚未初始化的数据,就会出现时序问题
        }
    }

    fun onStop() {
        scope?.cancel()
        scope = null
    }
}

你看,如果 onStart 和 onStop 的调用顺序出现了乱序,或者 scope 在创建后还没来得及启动协程就被取消了,那就会导致协程根本没执行。我们在后续章节里会反复遇到这类问题,而根源往往就是 scope 的创建和取消没有对齐生命周期。

核心原则:scope 的创建应该尽可能早(比如在对象构造时),取消应该尽可能晚(不使用时立即取消)。

2.5 知识结构梳理:从依赖到作用域

下面这张图总结了我们这一章的脉络。你把它理解成一个「环境搭建三步走」的流程:先配依赖,再建 scope,最后用 scope 启动协程。

Kotlin 协程环境搭建核心流程 第一阶段:环境准备 第二阶段:Scope 创建 第三阶段:协程运行 1. 创建 Kotlin 项目 2. 添加协程依赖 3. 选择 scope 类型 4. 创建 CoroutineScope 5. 使用 launch/async 关键提示 • 务必先确认 Kotlin 版本与协程库版本兼容,否则编译报错 • 自定义 scope 时推荐使用 SupervisorJob 组合,防止子协程异常扩散 • scope 的生命周期要与使用者一致,避免内存泄漏或任务丢失

这张图你可以截图存着,后面遇到协程启动时序问题时,先回来看看 scope 是在哪个阶段创建的——很多时候问题就出在阶段 2 和阶段 3 之间的衔接上。

2.6 本章小结

我们做了三件事:

  • 添加了 kotlinx-coroutines-core 依赖
  • 理解了 CoroutineScope 的本质及其生命周期管理
  • 创建了自定义 scope,并掌握了创建时机对时序的影响

这些就是协程大楼的地基。下一块砖,我们会开始讨论协程启动的具体时机——比如 launch 一调用就立即执行吗?如果不是,那延迟在哪里?

不过别急,在动手写第一个协程之前,请确保你环境里的依赖版本是匹配的。我偶尔会看到有人把 kotlinx-coroutines-core 写成了 kotlinx-coroutine-core(少了一个 s),编译时对着红字发半天呆……嗯,别问我怎么知道的。