1. 协程是什么
2. Android 为什么需要协程
3. suspend 挂起原理
4. CoroutineScope / Job / Dispatcher / CoroutineContext
5. launch / async / withContext 区别
6. 结构化并发
7. 取消机制
8. 异常处理
9. Android 生命周期中的协程
10. Flow 和协程的关系
11. 协程底层原理
12. 面试常问题
一、协程是什么?
Kotlin 协程是一套用于处理异步任务的并发框架。
它的核心价值是:
用看起来像同步的代码,写出非阻塞的异步逻辑。
比如以前 Android 网络请求可能这样写:
api.getUser(object : Callback<User> {
override fun onSuccess(user: User) {
runOnUiThread {
textView.text = user.name
}
}
override fun onError(e: Throwable) {
showError()
}
})
协程后可以写成:
lifecycleScope.launch {
try {
val user = withContext(Dispatchers.IO) {
api.getUser()
}
textView.text = user.name
} catch (e: Exception) {
showError(e)
}
}
看起来是同步顺序:
请求用户数据
↓
拿到结果
↓
更新 UI
但实际上不会阻塞主线程。
二、协程解决 Android 中什么问题?
Android 中最核心的问题是:
主线程不能做耗时操作,否则会 ANR。
常见耗时操作包括:
- 网络请求;
- 数据库读写;
- 文件 IO;
- 图片处理;
- 大量计算;
- 延迟任务。
以前可以用:
- Thread;
- Handler;
- AsyncTask;
- RxJava;
- 回调 Callback。
但这些方式都有问题:
| 方式 | 问题 |
|---|---|
| Thread | 创建成本高,管理麻烦 |
| Handler | 嵌套多,容易泄漏 |
| AsyncTask | 已废弃 |
| Callback | 回调地狱,可读性差 |
| RxJava | 功能强,但学习成本高 |
| 协程 | 轻量、结构清晰、生命周期友好 |
所以现在 Android 官方推荐:
ViewModel + Coroutine + Flow + Repository
三、协程和线程的关系
面试中经常问:
协程是线程吗?
答案:
协程不是线程,协程运行在线程之上。线程是真正执行代码的执行单元;协程是被调度到线程上执行的一段可挂起、可恢复的任务。可以理解为协程是一段任务,要依附线程进行执行。
可以这样理解:
线程:CPU 调度的基本单位
协程:用户态的轻量 # 任务 #,由协程调度器调度
一个线程可以运行很多协程。
例如:
repeat(100_000) {
GlobalScope.launch {
delay(1000)
println(it)
}
}
创建 10 万个协程一般没问题。
但如果创建 10 万个线程:
repeat(100_000) {
Thread {
Thread.sleep(1000)
}.start()
}
大概率会内存爆炸。
因为线程有独立栈空间,创建和切换成本高;协程的挂起恢复成本低很多。
四、挂起 suspend 是什么?
suspend 的意思是:
这个函数可以挂起当前协程,但不会阻塞当前线程。
例如:
suspend fun getUser(): User {
delay(1000)
return User("Tom")
}
执行到 delay(1000) 时:
当前协程挂起
线程被释放,可以去执行其他任务
1 秒后协程恢复
继续执行 return User("Tom")
重点:
挂起的是协程,不是线程。
1. delay 和 Thread.sleep 的区别
delay(3000)
表示:
挂起协程 3 秒,不阻塞线程
而:
Thread.sleep(3000)
表示:
阻塞当前线程 3 秒
Android 主线程中:
lifecycleScope.launch {
delay(3000)
textView.text = "完成"
}
不会卡 UI。
但:
lifecycleScope.launch {
Thread.sleep(3000)
textView.text = "完成"
}
会卡住主线程,可能导致 ANR。
2. suspend 函数能在哪里调用?
suspend 函数只能在:
1. 另一个 suspend 函数中调用
2. 协程体中调用,比如 launch / async
例如:
suspend fun requestData() {
delay(1000)
}
不能直接在普通函数里调用:
fun test() {
requestData() // 错误
}
必须这样:
lifecycleScope.launch {
requestData()
}
或者:
suspend fun test() {
requestData()
}
五、协程四大核心概念
Android 面试中要重点掌握:
CoroutineScope
CoroutineContext
Job
Dispatcher
六、CoroutineScope:协程作用域
CoroutineScope 表示协程的作用范围。
它决定:
协程什么时候创建
协程什么时候取消
协程的生命周期归谁管理
常见作用域:
| Scope | 使用位置 | 生命周期 |
|---|---|---|
lifecycleScope |
Activity / Fragment | 跟随 Lifecycle |
viewModelScope |
ViewModel | ViewModel 清除时取消 |
GlobalScope |
全局 | 应用进程级,不推荐 |
| 自定义 Scope | 特殊场景 | 手动管理 |
1. lifecycleScope
在 Activity / Fragment 中使用:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
delay(1000)
textView.text = "Hello"
}
}
}
当 Activity 销毁时,lifecycleScope 中的协程会自动取消。
2. viewModelScope
在 ViewModel 中使用:
class UserViewModel : ViewModel() {
fun loadUser() {
viewModelScope.launch {
val user = repository.getUser()
// 更新 LiveData / StateFlow
}
}
}
当 ViewModel 执行 onCleared() 时,viewModelScope 会自动取消。
这是 Android MVVM 中最常见写法。
3. 为什么不推荐 GlobalScope?
GlobalScope.launch {
delay(10000)
}
GlobalScope 的生命周期和应用进程一样长,不跟 Activity、Fragment、ViewModel 绑定。
问题:
- 容易内存泄漏;
- 页面销毁后任务还在执行;
- 不符合结构化并发;
- 异常不好管理;
- 取消困难。
除非是明确需要应用级后台任务,否则不推荐使用 GlobalScope。在 Android 中通常使用 lifecycleScope、viewModelScope 或自定义受控 Scope。
七、CoroutineContext:协程上下文
CoroutineContext 是协程运行环境的集合。
它里面可以包含:
Job
Dispatcher
CoroutineName
CoroutineExceptionHandler
例如:
val context = Dispatchers.IO + Job() + CoroutineName("request")
启动协程:
CoroutineScope(context).launch {
// 协程代码
}
可以理解为:
CoroutineContext 是协程的配置集合。
八、Job:协程任务
每启动一个协程,都会有一个 Job。
val job = lifecycleScope.launch {
delay(5000)
println("完成")
}
可以通过 Job 管理协程:
job.cancel()
job.join()
1. cancel
取消协程:
job.cancel()
2. join
等待协程完成:
job.join()
3. cancelAndJoin
取消并等待它结束:
job.cancelAndJoin()
4. Job 的父子关系
协程有父子关系:
val parentJob = lifecycleScope.launch {
launch {
delay(1000)
println("child 1")
}
launch {
delay(2000)
println("child 2")
}
}
这里内部两个 launch 是外部协程的子协程。
父协程取消时,子协程也会取消。
子协程异常时,通常会影响父协程。
这就是结构化并发的基础。
九、Dispatcher:调度器
Dispatcher 决定协程运行在哪个线程或线程池上。
常见 Dispatcher:
| Dispatcher | 作用 |
|---|---|
Dispatchers.Main |
Android 主线程,更新 UI |
Dispatchers.IO |
IO 密集型任务,例如网络、数据库、文件 |
Dispatchers.Default |
CPU 密集型任务,例如排序、JSON 解析、大计算 |
Dispatchers.Unconfined |
不限制线程,不常用 |
| 自定义 Dispatcher | 特殊线程池 |
1. Dispatchers.Main
用于 UI 操作:
lifecycleScope.launch(Dispatchers.Main) {
textView.text = "Hello"
}
在 Android 中,lifecycleScope.launch {} 默认就是主线程。
2. Dispatchers.IO
用于 IO 操作:
withContext(Dispatchers.IO) {
database.userDao().getUsers()
}
适合:
- Retrofit 网络请求;
- Room 数据库;
- 文件读写;
- SharedPreferences/DataStore 操作。
3. Dispatchers.Default
用于 CPU 密集任务:
withContext(Dispatchers.Default) {
bigList.sorted()
}
适合:
- 大量计算;
- 图片压缩;
- JSON 大数据解析;
- 排序;
- 加密解密。
4. 面试常问:IO 和 Default 区别?
可以这样答:
Dispatchers.IO 主要用于阻塞式 IO 任务,线程池弹性较大;Dispatchers.Default 主要用于 CPU 密集型任务,线程数通常接近 CPU 核心数。网络、数据库、文件读写用 IO;计算、排序、解析大数据用 Default。
十、launch、async、withContext 区别
这是高频面试题。
1. launch
launch 启动一个协程,不返回结果,只返回 Job。
val job = lifecycleScope.launch {
delay(1000)
println("完成")
}
适合:
执行一个不需要返回值的异步任务
比如:
- 请求数据后更新 UI;
- 记录日志;
- 执行一个后台任务。
2. async
async 启动一个协程,返回 Deferred<T>,可以通过 await() 获取结果。
val deferred = lifecycleScope.async {
delay(1000)
"结果"
}
val result = deferred.await()
适合:
并发执行多个任务,并获取结果
例如:
lifecycleScope.launch {
val userDeferred = async { repository.getUser() }
val orderDeferred = async { repository.getOrders() }
val user = userDeferred.await()
val orders = orderDeferred.await()
showData(user, orders)
}
这里两个请求是并发的。
3. withContext
withContext 用于切换协程上下文,并返回结果。
val user = withContext(Dispatchers.IO) {
api.getUser()
}
它不会创建新的并发任务来让你手动管理,而是在当前协程中切换上下文执行。
常见于:
主线程中切换到 IO 线程请求数据,然后回到主线程更新 UI
例如:
lifecycleScope.launch {
val user = withContext(Dispatchers.IO) {
repository.getUser()
}
textView.text = user.name
}
4. 三者对比
| API | 是否返回结果 | 是否新建协程 | 典型用途 |
|---|---|---|---|
launch |
不直接返回结果 | 是 | 启动任务 |
async |
返回 Deferred | 是 | 并发任务取结果 |
withContext |
返回代码块结果 | 不算新的独立任务 | 切线程/切上下文 |
面试简答:
launch 用于启动不关心返回值的协程;async 用于并发执行并通过 await 获取结果;withContext 通常用于在当前协程中切换线程或上下文,并直接返回执行结果。
十一、结构化并发
结构化并发是 Kotlin 协程非常核心的设计思想。
它要求:
协程必须运行在明确的作用域中,父协程管理子协程,任务有边界,生命周期可控。
例如:
viewModelScope.launch {
launch {
repository.requestA()
}
launch {
repository.requestB()
}
}
这里两个子协程属于 viewModelScope.launch 的父协程。
结构化并发带来的好处:
1. 父协程取消,子协程全部取消
2. 父协程会等待子协程完成
3. 异常可以向上传播
4. 避免任务失控
1. coroutineScope
suspend fun 中如果要启动多个子协程,可以用:
suspend fun loadData() = coroutineScope {
val user = async { repository.getUser() }
val orders = async { repository.getOrders() }
user.await() to orders.await()
}
coroutineScope 的特点:
子协程全部完成后,coroutineScope 才返回
任意一个子协程失败,会取消其他兄弟协程
2. supervisorScope
如果希望一个子协程失败,不影响其他子协程,可以用:
suspend fun loadData() = supervisorScope {
val banner = async { repository.getBanner() }
val news = async { repository.getNews() }
val bannerResult = runCatching { banner.await() }
val newsResult = runCatching { news.await() }
bannerResult to newsResult
}
supervisorScope 的特点:
子协程失败,不会自动取消其他兄弟协程
适合页面上多个模块独立加载。
十二、协程取消机制
协程取消也是面试重点。
协程取消是:
协作式取消。
也就是说:
调用 job.cancel() 后,并不是粗暴杀死线程,而是给协程打一个取消标记。
协程需要在挂起点或主动检查取消状态时响应取消。
1. 可取消的挂起函数
这些函数通常是可取消的:
delay()
withContext()
join()
await()
Flow.collect()
例如:
val job = lifecycleScope.launch {
repeat(10) {
delay(1000)
println(it)
}
}
job.cancel()
执行到下一个 delay() 时,会响应取消。
2. CPU 循环中如何响应取消?
如果是长时间计算,没有挂起点:
lifecycleScope.launch(Dispatchers.Default) {
while (true) {
// 大量计算
}
}
即使调用 cancel(),它也可能不会马上停止。
应该这样:
lifecycleScope.launch(Dispatchers.Default) {
while (isActive) {
// 大量计算
}
}
或者:
lifecycleScope.launch(Dispatchers.Default) {
for (i in 1..1000000) {
ensureActive()
// 计算
}
}
也可以偶尔:
yield()
让出执行权并检查取消。
3. CancellationException
协程取消时会抛出:
CancellationException
它是正常取消信号,不应该当成普通错误处理。
错误示例:
try {
delay(1000)
} catch (e: Exception) {
// 这里会捕获取消异常,可能导致取消被吞掉
}
更好的写法:
try {
delay(1000)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
showError(e)
}
或者:
runCatching {
repository.getUser()
}.onFailure {
if (it is CancellationException) throw it
showError(it)
}
面试可以说:
CancellationException 表示协程正常取消,不应该被吞掉。如果 catch Exception,要注意重新抛出 CancellationException。
十三、异常处理
协程异常处理很容易考。
1. launch 的异常
launch 中未捕获异常会直接抛给父协程。
lifecycleScope.launch {
throw RuntimeException("error")
}
如果不处理,可能导致父协程取消。
可以用:
lifecycleScope.launch {
try {
repository.getUser()
} catch (e: Exception) {
showError(e)
}
}
或者使用 CoroutineExceptionHandler:
val handler = CoroutineExceptionHandler { _, throwable ->
Log.e("Coroutine", "error", throwable)
}
lifecycleScope.launch(handler) {
throw RuntimeException("error")
}
注意:
CoroutineExceptionHandler 只处理未捕获异常,通常对根协程有效。子协程异常会先向上传播。
2. async 的异常
async 的异常会暂存在 Deferred 中,等调用 await() 时抛出。
val deferred = lifecycleScope.async {
throw RuntimeException("error")
}
try {
deferred.await()
} catch (e: Exception) {
showError(e)
}
面试简答:
launch 的异常如果未捕获会立即传播;async 的异常通常会在 await 时重新抛出。
不过要注意,在结构化并发中,async 子协程失败也可能导致父协程取消,所以不要简单理解成完全等到 await 才影响父协程。
3. coroutineScope 和 supervisorScope 的异常区别
coroutineScope {
launch {
throw RuntimeException()
}
launch {
delay(1000)
println("不会执行")
}
}
第一个子协程异常,会取消整个 scope,第二个也被取消。
supervisorScope {
launch {
throw RuntimeException()
}
launch {
delay(1000)
println("仍然可能执行")
}
}
在 supervisorScope 中,一个子协程失败,不会自动取消兄弟协程。
十四、Android 生命周期中的协程
Android 中不要随便创建裸协程,要和生命周期绑定。
1. ViewModel 中请求数据
常见写法:
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
val uiState: StateFlow<UserUiState> = _uiState
fun loadUser() {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
val result = runCatching {
repository.getUser()
}
_uiState.value = result.fold(
onSuccess = { UserUiState.Success(it) },
onFailure = { UserUiState.Error(it.message ?: "加载失败") }
)
}
}
}
2. Activity / Fragment 收集 StateFlow
推荐:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
render(state)
}
}
}
为什么用 repeatOnLifecycle?
因为它会:
Lifecycle 到 STARTED 时开始收集
Lifecycle 低于 STARTED 时取消收集
再次 STARTED 时重新收集
避免:
- 页面不可见还在收集;
- 内存泄漏;
- UI 更新时机错误。
3. launchWhenStarted 为什么不如 repeatOnLifecycle?
以前常见:
lifecycleScope.launchWhenStarted {
viewModel.uiState.collect {
render(it)
}
}
现在更推荐:
repeatOnLifecycle(Lifecycle.State.STARTED)
原因是:
launchWhenStarted 在生命周期停止时可能只是挂起外层协程,而上游 Flow 仍可能继续工作;repeatOnLifecycle 会在 STOPPED 时取消 block,重新 STARTED 时再启动,更符合资源管理。
十五、Repository 中如何写 suspend?
Repository 一般暴露 suspend 函数或 Flow。
class UserRepository(
private val api: ApiService,
private val dao: UserDao
) {
suspend fun getUser(id: String): User {
return withContext(Dispatchers.IO) {
val local = dao.getUser(id)
if (local != null) {
local
} else {
val remote = api.getUser(id)
dao.insert(remote)
remote
}
}
}
}
不过现在很多团队认为:
Repository 不一定每个方法都强行 withContext(IO),如果底层库已经支持 suspend 并自己切线程,可以避免重复切换。
例如 Retrofit 的 suspend 接口通常已经异步,不阻塞调用线程;Room 的 suspend DAO 也会处理线程。
但面试中稳妥说法是:
耗时或阻塞操作不要放在 Main 线程。可以在 Repository 或 UseCase 中使用 withContext(Dispatchers.IO),但也要结合底层库实现,避免不必要的线程切换。
十六、Flow 和协程关系
Flow 是 Kotlin 协程体系中的异步数据流。
如果说:
suspend 函数返回一次结果
Flow 可以返回多次结果
例如:
suspend fun getUser(): User
返回一次。
而:
fun observeUser(): Flow<User>
可以持续返回:
用户 A
用户 B
用户 C
...
1. Flow 简单示例
fun countDown(): Flow<Int> = flow {
for (i in 3 downTo 1) {
emit(i)
delay(1000)
}
}
收集:
lifecycleScope.launch {
countDown().collect { value ->
println(value)
}
}
2. Flow 是冷流
默认 Flow 是冷流。
意思是:
不 collect,就不会执行。
val flow = flow {
println("开始执行")
emit(1)
}
flow.collect {
println(it)
}
只有调用 collect 后,flow {} 里面的代码才会执行。
每次 collect 都会重新执行一次。
3. StateFlow 和 SharedFlow
Android MVVM 中常见:
StateFlow:表示状态
SharedFlow:表示事件
StateFlow
适合 UI 状态:
data class LoginUiState(
val loading: Boolean = false,
val user: User? = null,
val error: String? = null
)
ViewModel:
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState
特点:
有初始值
保存最新值
新订阅者立刻收到最新值
适合页面状态
SharedFlow
适合一次性事件:
private val _events = MutableSharedFlow<LoginEvent>()
val events: SharedFlow<LoginEvent> = _events
例如:
viewModelScope.launch {
_events.emit(LoginEvent.NavigateHome)
}
适合:
- Toast;
- Snackbar;
- 页面跳转;
- 弹窗事件。
十七、协程底层原理
面试如果问深层次,可以这样答。
Kotlin 协程底层主要依赖:
Continuation
状态机
协程调度器
1. suspend 函数会被编译器改造
例如你写:
suspend fun foo(): String {
val a = requestA()
val b = requestB()
return a + b
}
编译器大致会把它改造成一个带 Continuation 参数的函数。
可以粗略理解成:
fun foo(continuation: Continuation<String>): Any
它的返回值可能是:
真正结果
或者 COROUTINE_SUSPENDED
如果遇到挂起点,就返回 COROUTINE_SUSPENDED,表示当前协程挂起。
2. Continuation 是什么?
Continuation 可以理解为:
挂起后,如何继续执行后续代码的对象。
例如:
suspend fun test() {
println("A")
delay(1000)
println("B")
}
执行到 delay(1000):
保存当前状态
返回挂起标记
线程去执行别的任务
1 秒后调度器通过 Continuation 恢复
继续执行 println("B")
3. 状态机
多个挂起点会被编译成状态机。
suspend fun test() {
println("1")
delay(1000)
println("2")
delay(1000)
println("3")
}
可以理解成:
label = 0:执行 println("1"),遇到第一个 delay,挂起
label = 1:恢复后执行 println("2"),遇到第二个 delay,挂起
label = 2:恢复后执行 println("3"),结束
局部变量也会被保存到状态机对象里。
所以协程可以暂停后恢复。
十八、常见面试题回答
1. 什么是协程?
答:
协程是 Kotlin 提供的一种轻量级并发方案。它不是线程,而是运行在线程之上的任务单元。协程可以在挂起点暂停执行,并在之后恢复,挂起时不会阻塞线程。在 Android 中,它主要用于网络、数据库、文件等异步任务,让异步代码写起来像同步代码,并且可以配合生命周期自动取消。
2. suspend 是什么?
答:
suspend 表示这个函数可以挂起协程。挂起不是阻塞线程,而是保存当前执行状态,把线程释放出去,等异步结果回来后再通过 Continuation 恢复执行。suspend 函数只能在协程或其他 suspend 函数中调用。
3. launch、async、withContext 区别?
答:
launch 用于启动一个不需要返回值的协程,返回 Job;async 用于启动一个需要返回值的协程,返回 Deferred,通过 await 获取结果,适合并发任务;withContext 用于在当前协程中切换上下文或线程,并返回代码块结果,常用于切到 IO 线程执行耗时操作。
4. Dispatchers.IO 和 Dispatchers.Default 区别?
答:
IO 用于网络、数据库、文件读写等 IO 密集任务,线程池相对弹性;Default 用于 CPU 密集任务,比如排序、计算、解析大 JSON,线程数通常接近 CPU 核心数。主线程更新 UI 用 Dispatchers.Main。
5. 协程如何取消?
答:
协程取消是协作式的。调用 Job.cancel() 后,会给协程设置取消状态。协程需要在挂起点,比如 delay、await、withContext,或者主动检查 isActive、ensureActive 时响应取消。取消时会抛出 CancellationException,它代表正常取消,不应该被普通 catch 吞掉。
6. 什么是结构化并发?
答:
结构化并发要求协程必须运行在明确的 CoroutineScope 中,父协程管理子协程。父协程取消时子协程也取消,父协程会等待子协程完成,异常可以按照父子关系传播。它避免了任务失控和内存泄漏,是 Kotlin 协程的重要设计原则。
7. coroutineScope 和 supervisorScope 区别?
答:
coroutineScope 中任意一个子协程失败,会取消整个作用域,其他兄弟协程也会被取消;supervisorScope 中一个子协程失败不会自动影响其他兄弟协程,适合多个互不依赖的任务,比如页面多个模块分别加载。
8. 协程异常如何处理?
答:
launch 中未捕获异常会向父协程传播,可以用 try-catch 或 CoroutineExceptionHandler 处理;async 的异常通常在 await 时抛出,也需要 try-catch。CoroutineExceptionHandler 一般只处理根协程的未捕获异常,子协程异常仍会先按结构化并发规则向上传播。
9. 为什么不推荐 GlobalScope?
答:
GlobalScope 的生命周期和应用进程绑定,不跟 Activity、Fragment、ViewModel 生命周期绑定,容易导致页面销毁后任务还在执行,造成内存泄漏或无效回调,而且不符合结构化并发。Android 中更推荐 lifecycleScope、viewModelScope 或自定义受控 Scope。
10. Flow、StateFlow、SharedFlow 区别?
答:
Flow 是冷流,不 collect 就不执行,适合表示异步数据流。StateFlow 是热流,有初始值,保存最新状态,新订阅者会立即收到最新值,适合 UI State。SharedFlow 也是热流,可以配置 replay 和缓冲,适合事件分发,比如 Toast、导航、Snackbar。
十九、Android 协程推荐写法
一个比较标准的 MVVM 写法:
class LoginViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState
private val _event = MutableSharedFlow<LoginEvent>()
val event: SharedFlow<LoginEvent> = _event
fun login(username: String, password: String) {
viewModelScope.launch {
if (username.isBlank() || password.isBlank()) {
_event.emit(LoginEvent.ShowToast("用户名或密码不能为空"))
return@launch
}
_uiState.value = _uiState.value.copy(loading = true)
try {
val user = repository.login(username, password)
_uiState.value = _uiState.value.copy(
loading = false,
user = user
)
_event.emit(LoginEvent.NavigateHome)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(loading = false)
_event.emit(LoginEvent.ShowToast(e.message ?: "登录失败"))
}
}
}
}
Activity / Fragment:
class LoginFragment : Fragment() {
private val viewModel by viewModels<LoginViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.uiState.collect { state ->
render(state)
}
}
launch {
viewModel.event.collect { event ->
handleEvent(event)
}
}
}
}
}
}
注意 Fragment 中一般用:
viewLifecycleOwner.lifecycleScope
而不是直接用:
lifecycleScope
因为 Fragment 的生命周期和 Fragment View 的生命周期不完全一样。
二十、最后给你一个面试总结版
你可以这样总结 Android 协程:
Kotlin 协程是运行在线程之上的轻量级并发框架,核心是 suspend 挂起函数。挂起会保存当前执行状态并释放线程,之后通过 Continuation 恢复执行,所以它可以用同步代码风格写异步逻辑。在 Android 中,协程主要用于网络请求、数据库、文件 IO 等耗时任务,避免阻塞主线程。
协程通过 CoroutineScope 管理生命周期,通过 Job 管理任务,通过 Dispatcher 决定运行线程,通过 CoroutineContext 组合上下文。Android 中常用 viewModelScope 和 lifecycleScope,不推荐随意使用 GlobalScope。
launch 适合启动无返回值任务,async 适合并发任务并通过 await 获取结果,withContext 适合切换线程并返回结果。协程遵循结构化并发,父协程会管理子协程,取消和异常会按父子关系传播。取消是协作式的,CancellationException 表示正常取消,不应该被吞掉。
在现代 Android 架构中,协程通常和 ViewModel、Repository、Flow、StateFlow、SharedFlow、repeatOnLifecycle 一起使用,实现生命周期安全、可取消、可读性好的异步编程。