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.Mainlifecycle-viewmodel-ktx:提供viewModelScopelifecycle-runtime-ktx:提供lifecycleScope、repeatOnLifecyclekotlinx-coroutines-test:用于协程测试
2. 协程在 Android 中是干什么的
协程是 Kotlin 提供的轻量级异步并发方案。在 Android 中,它主要用于:
- 发起网络请求
- 访问数据库
- 切换主线程和后台线程
- 监听 UI 状态变化
- 配合
ViewModel和Lifecycle安全管理异步任务
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 中,最常见的是把作用域绑定到 ViewModel 或 LifecycleOwner。
常见作用域:
coroutineScope {}:子协程失败会取消同级和父作用域supervisorScope {}:子协程失败不会影响兄弟协程viewModelScope:跟随ViewModel生命周期lifecycleScope:跟随Activity或Fragment生命周期
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 状态 - 不要在
Fragment或Activity里堆积大量异步逻辑 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 没有 StateFlow 和 SharedFlow 常用,学习阶段可以先把重点放在 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
适合在 Activity 或 Fragment 中执行和界面生命周期直接绑定的协程。
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-test 的 runTest,它支持虚拟时间,测试更快更稳定。
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)
}
}
常用测试能力:
runTestadvanceTimeBy(...)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 - 在
Activity或Fragment收集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 -> RepositoryRepository -> 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 为什么推荐这种写法
好处主要有这些:
- 页面状态集中管理,调试更容易
Fragment或Activity更轻,只负责渲染- 配置变化后,
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 - 用
LiveData、StateFlow、SharedFlow混着写但职责不清 - 把 Toast、导航这种一次性事件塞进
StateFlow - 在
ViewModel中直接写大量数据库和网络实现细节 - 捕获了
CancellationException却没有重新抛出
15.8 学习阶段建议你先掌握这套最小组合
如果你是为了后面系统学习 Android,建议先把下面这套组合练熟:
viewModelScopeMutableStateFlowMutableSharedFlowrepeatOnLifecyclewithContext(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. 参考资料
- 官方库:
/kotlin/kotlinx.coroutines - 协程基础文档:https://github.com/kotlin/kotlinx.coroutines/blob/master/docs/topics/coroutines-basics.md
- 上下文与调度器:https://github.com/kotlin/kotlinx.coroutines/blob/master/docs/topics/coroutine-context-and-dispatchers.md
- 取消与超时:https://github.com/kotlin/kotlinx.coroutines/blob/master/docs/topics/cancellation-and-timeouts.md
- test 模块说明:https://github.com/kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md
- Android 协程官方指南:https://developer.android.com/topic/libraries/architecture/coroutines