安卓协程
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 一起使用,实现生命周期安全、可取消、可读性好的异步编程。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇