Android Kotlin 协程使用指南

1. Android 项目引入依赖

kotlin 复制代码
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")

    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")

    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
}
kotlin 复制代码
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

这些依赖的作用:

  • kotlinx-coroutines-core:协程核心能力
  • kotlinx-coroutines-android:提供 Android 上的 Dispatchers.Main
  • lifecycle-viewmodel-ktx:提供 viewModelScope
  • lifecycle-runtime-ktx:提供 lifecycleScoperepeatOnLifecycle
  • kotlinx-coroutines-test:用于协程测试

2. 协程在 Android 中是干什么的

协程是 Kotlin 提供的轻量级异步并发方案。在 Android 中,它主要用于:

  • 发起网络请求
  • 访问数据库
  • 切换主线程和后台线程
  • 监听 UI 状态变化
  • 配合 ViewModelLifecycle 安全管理异步任务

3. 最常用的几个入口

suspend

  • 挂起函数,只能在协程或其他 suspend 函数里调用
  • 不会阻塞线程,只会挂起当前协程

launch

  • 启动一个不返回结果的协程
  • 返回 Job

async

  • 启动一个有结果的协程
  • 返回 Deferred<T>,通过 await() 取值

runBlocking

  • 把普通阻塞代码桥接到协程世界
  • 主要用于示例代码或测试,不建议在 Android 业务代码里使用
kotlin 复制代码
fun main() = runBlocking {
    launch {
        delay(500)
        println("launch done")
    }

    val result = async {
        delay(300)
        42
    }

    println(result.await())
}

4. Android 中的 CoroutineScope 和结构化并发

推荐始终在一个 CoroutineScope 中启动协程,而不是到处裸开线程或裸开协程。在 Android 中,最常见的是把作用域绑定到 ViewModelLifecycleOwner

常见作用域:

  • coroutineScope {}:子协程失败会取消同级和父作用域
  • supervisorScope {}:子协程失败不会影响兄弟协程
  • viewModelScope:跟随 ViewModel 生命周期
  • lifecycleScope:跟随 ActivityFragment 生命周期
kotlin 复制代码
suspend fun loadData() = coroutineScope {
    val user = async { fetchUser() }
    val posts = async { fetchPosts() }
    user.await() to posts.await()
}
kotlin 复制代码
suspend fun loadPartialData() = supervisorScope {
    val user = async { fetchUser() }
    val ads = async { fetchAds() }
    user.await() to runCatching { ads.await() }.getOrNull()
}

5. Android 中 Dispatchers 怎么选

Dispatchers.Main

  • Android UI 线程
  • 用于更新界面、提交 UI 状态

Dispatchers.IO

  • IO 密集型任务
  • 例如数据库、文件、网络

Dispatchers.Default

  • CPU 密集型任务
  • 例如计算、排序、解析

Dispatchers.Unconfined

  • 一般不建议业务代码使用
kotlin 复制代码
suspend fun readFile(): String = withContext(Dispatchers.IO) {
    "content"
}

suspend fun calculate(): Int = withContext(Dispatchers.Default) {
    (1..1_000_000).sum()
}

6. withContext 的正确定位

withContext 用于切换上下文并等待结果,适合一个任务需要切线程执行的场景。

kotlin 复制代码
class UserRepository {
    suspend fun fetchUser(): String = withContext(Dispatchers.IO) {
        delay(100)
        "Alice"
    }
}

经验:

  • 需要结果时优先 withContext
  • 并行执行多个任务时优先 async

7. Job、取消、超时

协程取消是协作式的。可挂起点如 delay()yield() 会响应取消。Android 中这点很重要,因为页面销毁时应及时取消任务,避免内存泄漏和无效更新。

kotlin 复制代码
fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("working $i")
            delay(500)
        }
    }

    delay(1300)
    job.cancel()
    job.join()
}

常用 API:

  • job.cancel()
  • job.join()
  • job.cancelAndJoin()
  • withTimeout(1000) { ... }
  • withTimeoutOrNull(1000) { ... }
kotlin 复制代码
suspend fun fetchWithTimeout(): String? =
    withTimeoutOrNull(1000) {
        delay(1500)
        "OK"
    }

长循环里建议主动检查取消:

kotlin 复制代码
suspend fun doWork() = coroutineScope {
    launch {
        while (isActive) {
            delay(100)
        }
    }
}

注意:

  • 捕获 CancellationException 后通常要重新抛出
  • 否则会破坏取消传播

8. 异常处理

关键区别:

launch

  • 异常会立刻向上传播

async

  • 异常会在 await() 时抛出
kotlin 复制代码
val handler = CoroutineExceptionHandler { _, e ->
    println("caught: $e")
}

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)

scope.launch {
    error("boom")
}

推荐原则:

  • 组件级作用域用 SupervisorJob
  • 需要隔离失败时用 supervisorScope
  • async 一定记得 await(),否则异常容易被延后

Android 实战中更常见的思路是:

  • ViewModel 中捕获业务异常并更新 UI 状态
  • 不要在 FragmentActivity 里堆积大量异步逻辑
  • CancellationException 要继续抛出

9. Flow、StateFlow、SharedFlow 怎么选

Flow

  • 冷流
  • 每次收集都会重新执行上游
kotlin 复制代码
fun numbers(): Flow<Int> = flow {
    emit(1)
    emit(2)
    emit(3)
}
kotlin 复制代码
suspend fun collectFlow() {
    numbers()
        .map { it * 2 }
        .filter { it > 2 }
        .collect { println(it) }
}

StateFlow

  • 热流
  • 必须有初始值
  • 永远保存当前最新状态
  • 很适合 UI 状态
kotlin 复制代码
class Vm {
    private val _uiState = MutableStateFlow("idle")
    val uiState: StateFlow<String> = _uiState

    fun update() {
        _uiState.value = "loading"
    }
}

SharedFlow

  • 热流
  • 适合广播事件
  • 可配置 replay 和缓冲区
kotlin 复制代码
private val _events = MutableSharedFlow<String>(replay = 0, extraBufferCapacity = 1)
val events: SharedFlow<String> = _events

suspend fun sendEvent() {
    _events.emit("toast")
}

选择建议:

  • 单次异步结果:suspend
  • 一串异步数据:Flow
  • 可观察状态:StateFlow
  • 广播事件:SharedFlow

Android 中推荐习惯:

  • 页面状态用 StateFlow
  • 一次性事件如 Toast、导航、Snackbar 用 SharedFlow
  • Repository 层持续数据流用 Flow

10. Mutex、Channel 的使用场景

Mutex

  • 用于保护共享可变状态
  • 类似协程版锁
kotlin 复制代码
val mutex = Mutex()
var count = 0

suspend fun inc() {
    mutex.withLock {
        count++
    }
}

Channel

  • 协程之间通信
  • 适合生产者/消费者模型
kotlin 复制代码
val channel = Channel<Int>()

suspend fun producer() {
    repeat(3) { channel.send(it) }
    channel.close()
}

suspend fun consumer() {
    for (x in channel) {
        println(x)
    }
}

经验:

  • 保护共享状态,用 Mutex
  • 传递消息,用 Channel
  • UI 或响应式状态管理,优先 Flow/StateFlow/SharedFlow

在 Android 日常开发里,Channel 没有 StateFlowSharedFlow 常用,学习阶段可以先把重点放在 Flow 体系上。

11. Android 中最常用的几个作用域

viewModelScope

适合放页面相关业务逻辑,页面旋转等配置变化时通常不会立即中断,ViewModel 清除时会自动取消。

kotlin 复制代码
class UserViewModel(
    private val repo: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow("idle")
    val state: StateFlow<String> = _state

    fun loadUser() {
        viewModelScope.launch {
            _state.value = "loading"
            _state.value = try {
                repo.fetchUser()
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                "error: ${e.message}"
            }
        }
    }
}

lifecycleScope

适合在 ActivityFragment 中执行和界面生命周期直接绑定的协程。

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            delay(500)
        }
    }
}

repeatOnLifecycle

收集 Flow 时推荐搭配它使用,界面不可见时会自动停止收集,可见时重新开始。

kotlin 复制代码
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.state.collect { state ->
            println(state)
        }
    }
}

12. 测试协程

官方推荐使用 kotlinx-coroutines-testrunTest,它支持虚拟时间,测试更快更稳定。

kotlin 复制代码
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals

class MyTest {
    @Test
    fun testDelaysAreSkipped() = runTest {
        val result = async {
            delay(1000)
            "Result"
        }
        assertEquals("Result", result.await())
        assertEquals(1000L, currentTime)
    }
}

常用测试能力:

  • runTest
  • advanceTimeBy(...)
  • advanceUntilIdle()
  • runCurrent()
kotlin 复制代码
@Test
fun testVirtualTime() = runTest {
    var value = 0
    launch {
        delay(1000)
        value = 1
        delay(1000)
        value = 2
    }

    advanceTimeBy(1000)
    runCurrent()
    assertEquals(1, value)

    advanceUntilIdle()
    assertEquals(2, value)
}

13. Android 协程最佳实践

  • 不要在业务层随意使用 GlobalScope
  • 不要把 runBlocking 塞进 Android UI 正常业务逻辑
  • CPU 任务放 Default,IO 任务放 IO
  • 能用结构化并发,就不要手工管理零散协程
  • async 必须配对 await()
  • 状态用 StateFlow,事件用 SharedFlow
  • 取消异常不要吞掉,通常应继续抛出
  • 页面逻辑优先放到 viewModelScope
  • ActivityFragment 收集 Flow 时优先使用 repeatOnLifecycle
  • 不要在 Fragment 里直接发起大量网络和数据库逻辑
  • 优先采用 UI -> ViewModel -> Repository 的分层写法

14. 一个 Android 实战模板

kotlin 复制代码
class UserRepository {
    suspend fun loadUser(): String = withContext(Dispatchers.IO) {
        delay(300)
        "Alice"
    }
}

class UserViewModel(
    private val repo: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow("idle")
    val state: StateFlow<String> = _state

    fun refresh() {
        viewModelScope.launch {
            _state.value = "loading"
            _state.value = try {
                val user = repo.loadUser()
                "success: $user"
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                "error: ${e.message}"
            }
        }
    }
}

页面层收集状态:

kotlin 复制代码
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.state.collect { state ->
                    println(state)
                }
            }
        }
    }
}

15. 协程与 ViewModel 的实现思路

在 Android 项目里,协程最常见的落点就是 ViewModel。推荐把页面异步逻辑放进 ViewModel,由它负责:

  • 发起异步请求
  • 保存页面状态
  • 向页面发送一次性事件
  • 屏蔽线程切换细节
  • 在页面销毁时自动取消任务

15.1 一个推荐的数据流向

推荐采用下面这条链路:

  • UI 负责展示和响应点击
  • ViewModel 负责状态流转和业务编排
  • Repository 负责数据获取

也就是:

  • UI -> ViewModel -> Repository
  • Repository -> ViewModel -> UI State

15.2 ViewModel 中区分 State 和 Event

推荐把页面中的数据拆成两类:

  • StateFlow:页面持续状态
  • SharedFlow:一次性事件

比如:

  • 列表数据、加载中、错误文案,放 StateFlow
  • Toast、Snackbar、跳转页面,放 SharedFlow
kotlin 复制代码
data class UserUiState(
    val loading: Boolean = false,
    val userName: String = "",
    val errorMessage: String? = null
)

sealed interface UserEvent {
    data class ShowToast(val message: String) : UserEvent
    data object NavigateToDetail : UserEvent
}

class UserViewModel(
    private val repo: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState

    private val _event = MutableSharedFlow<UserEvent>()
    val event: SharedFlow<UserEvent> = _event

    fun loadUser() {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(
                loading = true,
                errorMessage = null
            )

            try {
                val user = repo.loadUser()
                _uiState.value = _uiState.value.copy(
                    loading = false,
                    userName = user
                )
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                _uiState.value = _uiState.value.copy(
                    loading = false,
                    errorMessage = e.message
                )
                _event.emit(UserEvent.ShowToast("加载失败"))
            }
        }
    }
}

15.3 为什么推荐这种写法

好处主要有这些:

  • 页面状态集中管理,调试更容易
  • FragmentActivity 更轻,只负责渲染
  • 配置变化后,ViewModel 能保留状态
  • 协程跟随 viewModelScope 自动取消

15.4 Repository 负责 IO,ViewModel 不直接做脏活

推荐把网络、数据库、缓存读取这些工作放到 Repository 层,而不是直接写在 ViewModel 中。

kotlin 复制代码
class UserRepository(
    private val api: UserApi
) {
    suspend fun loadUser(): String = withContext(Dispatchers.IO) {
        api.getUser().name
    }
}

这样分层后:

  • Repository 负责拿数据
  • ViewModel 负责更新状态
  • UI 负责显示结果

15.5 在 ViewModel 中封装一个通用加载函数

当页面里有很多请求时,可以封装一个公共方法,减少重复的 try/catch

kotlin 复制代码
abstract class BaseViewModel : ViewModel() {

    protected fun launchSafely(
        onError: suspend (Throwable) -> Unit = {},
        block: suspend CoroutineScope.() -> Unit
    ) {
        viewModelScope.launch {
            try {
                block()
            } catch (e: CancellationException) {
                throw e
            } catch (e: Throwable) {
                onError(e)
            }
        }
    }
}

使用方式:

kotlin 复制代码
class UserViewModel(
    private val repo: UserRepository
) : BaseViewModel() {

    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState

    fun refresh() {
        launchSafely(
            onError = { error ->
                _uiState.value = _uiState.value.copy(
                    loading = false,
                    errorMessage = error.message
                )
            }
        ) {
            _uiState.value = _uiState.value.copy(loading = true, errorMessage = null)
            val user = repo.loadUser()
            _uiState.value = _uiState.value.copy(
                loading = false,
                userName = user
            )
        }
    }
}

15.6 页面层怎么收集 ViewModel 数据

页面层推荐分别收集状态和事件。

kotlin 复制代码
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    viewModel.uiState.collect { state ->
                        println("loading = ${state.loading}")
                        println("userName = ${state.userName}")
                    }
                }

                launch {
                    viewModel.event.collect { event ->
                        when (event) {
                            is UserEvent.ShowToast -> println(event.message)
                            UserEvent.NavigateToDetail -> println("navigate")
                        }
                    }
                }
            }
        }
    }
}

这里在 repeatOnLifecycle 内再开两个 launch,是因为:

  • 状态流和事件流需要并行收集
  • 任意一个 collect 都会持续挂起

15.7 ViewModel 中常见错误

  • 直接在 Fragment 里请求网络,不经过 ViewModel
  • LiveDataStateFlowSharedFlow 混着写但职责不清
  • 把 Toast、导航这种一次性事件塞进 StateFlow
  • ViewModel 中直接写大量数据库和网络实现细节
  • 捕获了 CancellationException 却没有重新抛出

15.8 学习阶段建议你先掌握这套最小组合

如果你是为了后面系统学习 Android,建议先把下面这套组合练熟:

  • viewModelScope
  • MutableStateFlow
  • MutableSharedFlow
  • repeatOnLifecycle
  • withContext(Dispatchers.IO)
  • Repository 分层

这套已经足够覆盖大多数 Android 协程基础场景。

15.9 用 sealed class 或 data class 管理页面状态

页面状态一般有两种常见建模方式。

第一种是单一 data class,适合表单页、详情页、列表页这类状态可以并存的场景:

kotlin 复制代码
data class ArticleUiState(
    val loading: Boolean = false,
    val list: List<String> = emptyList(),
    val errorMessage: String? = null
)

第二种是 sealed class,适合页面状态互斥非常明确的场景:

kotlin 复制代码
sealed interface ArticleState {
    data object Loading : ArticleState
    data class Success(val list: List<String>) : ArticleState
    data class Error(val message: String?) : ArticleState
}

经验上:

  • 大多数页面先用 data class
  • 明确只有加载中、成功、失败三种互斥状态时可以用 sealed class

15.10 ViewModel 初始化加载

很多页面一进入就需要加载数据,这时通常会在 init 中发起请求。

kotlin 复制代码
class ArticleViewModel(
    private val repo: ArticleRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(ArticleUiState())
    val uiState: StateFlow<ArticleUiState> = _uiState

    init {
        loadArticles()
    }

    fun loadArticles() {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(
                loading = true,
                errorMessage = null
            )

            try {
                val list = repo.loadArticles()
                _uiState.value = _uiState.value.copy(
                    loading = false,
                    list = list
                )
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                _uiState.value = _uiState.value.copy(
                    loading = false,
                    errorMessage = e.message
                )
            }
        }
    }
}

这类写法很常见,但也要注意:

  • 如果页面每次返回都会重新创建 ViewModel,就会重新加载
  • 如果你需要保留参数或恢复状态,可以配合 SavedStateHandle

15.11 ViewModel 配合 SavedStateHandle

当页面有路由参数、筛选条件、恢复状态需求时,可以在 ViewModel 中使用 SavedStateHandle

kotlin 复制代码
class DetailViewModel(
    savedStateHandle: SavedStateHandle,
    private val repo: UserRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState

    init {
        loadUser()
    }

    private fun loadUser() {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(loading = true)
            val user = repo.loadUserById(userId)
            _uiState.value = _uiState.value.copy(
                loading = false,
                userName = user
            )
        }
    }
}

这种方式很适合:

  • 详情页按 id 加载数据
  • 页面重建后恢复上一次关键参数

15.12 ViewModel 中处理搜索防抖

搜索框是协程和 Flow 在 Android 里非常典型的使用场景。

kotlin 复制代码
class SearchViewModel(
    private val repo: SearchRepository
) : ViewModel() {

    private val keyword = MutableStateFlow("")

    val uiState: StateFlow<SearchUiState> = keyword
        .debounce(300)
        .distinctUntilChanged()
        .flatMapLatest { query ->
            flow {
                emit(SearchUiState(loading = true))
                val result = repo.search(query)
                emit(SearchUiState(result = result))
            }.catch { e ->
                emit(SearchUiState(errorMessage = e.message))
            }
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = SearchUiState()
        )

    fun updateKeyword(value: String) {
        keyword.value = value
    }
}

data class SearchUiState(
    val loading: Boolean = false,
    val result: List<String> = emptyList(),
    val errorMessage: String? = null
)

这里几个操作符的意义:

  • debounce(300):用户停止输入 300ms 后再请求
  • distinctUntilChanged():内容没变就不重复搜索
  • flatMapLatest:新的搜索词来了,取消旧请求
  • stateIn:把流转换为可供页面直接收集的 StateFlow

15.13 ViewModel 中什么时候用 stateIn

当你有一个上游 Flow,希望把它变成页面长期观察的状态时,通常会用 stateIn

适合场景:

  • 搜索结果
  • 本地数据库数据流
  • 多个 Flow 合并后的页面状态

例如把用户信息和文章列表合并:

kotlin 复制代码
val uiState: StateFlow<HomeUiState> = combine(
    userRepository.userFlow(),
    articleRepository.articleFlow()
) { user, articles ->
    HomeUiState(
        userName = user.name,
        articles = articles
    )
}.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    initialValue = HomeUiState()
)

data class HomeUiState(
    val userName: String = "",
    val articles: List<String> = emptyList()
)

15.14 ViewModel 实现的一个简化模板

如果你后面开始自己写项目,可以先套这个最小模板:

kotlin 复制代码
data class PageState(
    val loading: Boolean = false,
    val data: String = "",
    val errorMessage: String? = null
)

sealed interface PageEvent {
    data class Toast(val message: String) : PageEvent
}

class PageViewModel(
    private val repo: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(PageState())
    val uiState: StateFlow<PageState> = _uiState

    private val _event = MutableSharedFlow<PageEvent>()
    val event: SharedFlow<PageEvent> = _event

    fun request() {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(
                loading = true,
                errorMessage = null
            )

            try {
                val result = repo.loadUser()
                _uiState.value = _uiState.value.copy(
                    loading = false,
                    data = result
                )
            } catch (e: CancellationException) {
                throw e
            } catch (e: Exception) {
                _uiState.value = _uiState.value.copy(
                    loading = false,
                    errorMessage = e.message
                )
                _event.emit(PageEvent.Toast("请求失败"))
            }
        }
    }
}

16. 参考资料

相关推荐
csbysj20202 小时前
jQuery 捕获详解
开发语言
C++ 老炮儿的技术栈2 小时前
GCC编译时无法向/tmp 目录写入临时汇编文件,因为设备空间不足,解决
linux·运维·开发语言·汇编·c++·git·qt
BLUcoding2 小时前
Android 布局介绍
android
三道渊2 小时前
进程通信与网络协议
开发语言·数据库·php
summerkissyou19872 小时前
android-蓝牙-状态和协议值总结及监听例子
android·蓝牙
徒 花2 小时前
数据库知识复习05
android·数据库
白露与泡影2 小时前
Java面试题库及答案解析(2026版)
java·开发语言·面试
疯狂成瘾者3 小时前
Chroma向量数据库
开发语言·数据库·c#
我是唐青枫3 小时前
C#.NET Monitor 与 Mutex 深入解析:进程内同步、跨进程互斥与使用边界
开发语言·c#·.net