18-Compose开发-副作用
Jetpack Compose 开发:副作用完全指南
副作用(Side Effect)是 Compose 开发中不可回避的核心概念。本文将系统讲解什么是副作用、为什么需要副作用,以及如何使用 Compose 提供的各种 Effect API 安全、高效地处理副作用,包含完整的代码示例、对比表格和最佳实践。
一、什么是副作用?为什么需要副作用?
1.1 什么是副作用?
在 Compose 中,副作用是指任何发生在可组合函数作用域之外的操作,它改变了程序的外部状态或与外部系统交互。典型的副作用包括:
- 网络请求
- 数据库操作
- 读写 SharedPreferences
- 启动定时器
- 注册/反注册监听器、广播接收器
- 更新其他非 Compose 管理的 UI(如传统 View)
Composable 函数本质是“纯函数”——相同的输入(状态)始终产生相同的输出(UI),且不产生任何外部影响。但实际开发中,我们不可避免地需要执行这些“非纯操作”,它们就是副作用。
1.2 为什么需要副作用?
Compose 的核心是重组(Recomposition)——当状态变化时,可组合函数会被重新执行。这意味着:
- 可组合函数可能被多次执行,且执行顺序不可预测
- 在可组合函数中直接执行副作用会导致严重问题:重复执行、内存泄漏、状态不一致
因此,Compose 提供了专门的 Effect API 来安全地处理副作用,确保它们:
- 在正确的时机执行(如首次组合、状态变化时)
- 在组件退出时自动清理
- 与可组合函数的生命周期绑定
核心原则:副作用必须封装在 Effect API 中,绝对不要在可组合函数中直接执行。
二、LaunchedEffect:在 Composable 中启动协程
LaunchedEffect 是最常用的副作用 API,用于在可组合函数的作用域内启动一个协程。该协程在组件进入组合时启动,在组件离开组合时自动取消。
2.1 基础用法
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import kotlinx.coroutines.delay
@Composable
fun TimerDemo() {
var countdown by remember { mutableStateOf(10) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("倒计时:$countdown 秒", fontSize = 24.sp)
LaunchedEffect(Unit) {
while (countdown > 0) {
delay(1000)
countdown--
}
}
}
}
2.2 key 参数的作用:当 key 变化时重新执行
LaunchedEffect 接受一个或多个 key 参数。当 key 的值发生变化时,当前协程会被取消,并重新启动一个新的协程。
- 若
key = Unit,协程仅在组件首次进入组合时启动一次,后续重组不会重启。 - 若
key是某个状态(如userId),则状态变化时协程会重新执行。
@Composable
fun UserProfile(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }
// key = userId:当 userId 变化时,重新请求数据
LaunchedEffect(userId) {
userData = fetchUserData(userId)
}
Text("用户: ${userData?.name ?: "加载中..."}")
}
常见 key 用法:
LaunchedEffect(Unit) { }— 只在首次组合时执行一次LaunchedEffect(searchQuery) { }— 当 searchQuery 变化时重新执行LaunchedEffect(userId, isEnabled) { }— 任一 key 变化都会重新执行
2.3 常见副作用场景:网络请求与定时器
网络请求(带状态管理)
@Composable
fun ProductListScreen() {
var uiState by remember { mutableStateOf(ProductUiState.Loading) }
LaunchedEffect(Unit) {
uiState = try {
val products = fetchProducts()
ProductUiState.Success(products)
} catch (e: Exception) {
ProductUiState.Error(e.message ?: "加载失败")
}
}
when (val state = uiState) {
is ProductUiState.Loading -> CircularProgressIndicator()
is ProductUiState.Success -> LazyColumn {
items(state.products) { product ->
Text(product.name)
}
}
is ProductUiState.Error -> Text(state.message)
}
}
定时器(倒计时)
@Composable
fun CountdownTimer(initialSeconds: Int, onFinish: () -> Unit) {
var currentSeconds by remember { mutableStateOf(initialSeconds) }
LaunchedEffect(initialSeconds) {
while (currentSeconds > 0) {
delay(1000)
currentSeconds--
}
onFinish()
}
Text("倒计时: $currentSeconds 秒", fontSize = 24.sp)
}
三、DisposableEffect:资源清理
DisposableEffect 用于管理需要清理的资源。它会在组件离开组合时调用 onDispose 块,确保资源被正确释放。
3.1 基础用法
import androidx.compose.runtime.DisposableEffect
@Composable
fun LocationTracker() {
var location by remember { mutableStateOf<Location?>(null) }
DisposableEffect(Unit) {
val locationListener = object : LocationListener {
override fun onLocationChanged(loc: Location) {
location = loc
}
}
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
1000L,
10f,
locationListener
)
onDispose {
locationManager.removeUpdates(locationListener)
}
}
location?.let {
Text("纬度: ${it.latitude}, 经度: ${it.longitude}")
}
}
3.2 应用场景:注册/反注册监听器、广播接收器
场景1:网络状态监听器
@Composable
fun NetworkStatusMonitor() {
val context = LocalContext.current
var isConnected by remember { mutableStateOf(false) }
DisposableEffect(Unit) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
isConnected = true
}
override fun onLost(network: Network) {
isConnected = false
}
}
connectivityManager.registerNetworkCallback(
NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build(),
networkCallback
)
onDispose {
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
Text("网络状态: ${if (isConnected) "已连接" else "未连接"}")
}
场景2:广播接收器
@Composable
fun BatteryStatusMonitor() {
var batteryLevel by remember { mutableStateOf(0) }
val context = LocalContext.current
DisposableEffect(Unit) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
batteryLevel = intent?.getIntExtra("level", 0) ?: 0
}
}
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
onDispose {
context.unregisterReceiver(receiver)
}
}
Text("当前电量: $batteryLevel%")
}
四、SideEffect:与外部非 Compose 世界同步
SideEffect 用于在重组完成后,将 Compose 状态同步到外部非 Compose 管理的对象。它会在每次成功重组后执行。
4.1 基础用法
@Composable
fun AnalyticsScreen(screenName: String) {
// 每次重组后,将当前屏幕名称同步到分析 SDK
SideEffect {
AnalyticsTracker.trackScreenView(screenName)
}
Text("当前屏幕: $screenName")
}
4.2 应用场景:同步 Compose 状态到原生 Activity
class SideEffectDemoActivity : ComponentActivity() {
var nativeCount = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SideEffectSyncDemo()
}
}
}
@Composable
fun SideEffectSyncDemo() {
val context = LocalContext.current as SideEffectDemoActivity
var composeCount by remember { mutableStateOf(0) }
SideEffect {
context.nativeCount = composeCount
println("同步 Compose 状态到原生:$composeCount")
}
Column {
Text("Compose 计数:$composeCount")
Text("原生 Activity 计数:${context.nativeCount}")
Button(onClick = { composeCount++ }) {
Text("增加计数")
}
}
}
五、其他常用的 Effect API
5.1 rememberCoroutineScope:在事件回调中启动协程
rememberCoroutineScope 返回一个协程作用域,用于在非重组回调中启动协程(如按钮点击)。
@Composable
fun SnackbarScreen() {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar("操作完成!")
}
},
modifier = Modifier.padding(paddingValues)
) {
Text("显示提示")
}
}
}
5.2 produceState:将外部状态转换为 Compose State
produceState 用于将非 Compose 状态源(如 Flow、LiveData、RxJava)转换为 Compose 可观察的 State。
@Composable
fun loadNetworkImage(url: String): State<ImageLoadState> {
return produceState(
initialValue = ImageLoadState.Loading,
key1 = url
) {
value = try {
val image = imageRepository.loadImage(url)
ImageLoadState.Success(image)
} catch (e: Exception) {
ImageLoadState.Error(e.message)
}
}
}
5.3 derivedStateOf:派生状态优化
derivedStateOf 用于创建依赖其他状态的派生状态,只有在依赖发生变化时才会重新计算。
@Composable
fun ScrollableList(items: List<String>) {
val listState = rememberLazyListState()
val showScrollToTopButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 5
}
}
Box {
LazyColumn(state = listState) {
items(items) { item -> Text(item) }
}
if (showScrollToTopButton) {
FloatingActionButton(
onClick = { /* 滚动到顶部 */ },
modifier = Modifier.align(Alignment.BottomEnd)
) {
Icon(Icons.Default.ArrowUpward, "回到顶部")
}
}
}
}
六、Effect API 对比总结
| API | 核心作用 | 是否支持协程 | 是否需要清理 | 执行时机 | 适用场景 |
|---|---|---|---|---|---|
| LaunchedEffect | 启动协程 | 是 | 自动取消 | 进入组合或 key 变化时 | 网络请求、定时器、数据流收集 |
| DisposableEffect | 资源管理 | 否 | 是,必须 onDispose | 进入组合或 key 变化时 | 注册/反注册监听器、广播接收器 |
| SideEffect | 同步外部状态 | 否 | 否 | 每次重组后 | 更新传统 View、日志记录、分析埋点 |
| rememberCoroutineScope | 获取协程作用域 | 是 | 组件退出时取消 | 组合期间 | 事件回调中的协程 |
| produceState | 转换外部状态 | 是 | 自动取消 | 进入组合或 key 变化时 | 将 Flow/LiveData 转为 State |
| derivedStateOf | 派生状态 | 否 | 否 | 依赖变化时 | 计算派生值、避免不必要的重组 |
七、常见陷阱与最佳实践
7.1 最佳实践
- 副作用必须封装在 Effect API 中,绝对不要在可组合函数中直接执行。
- 合理使用 key:通过 key 控制 Effect 的重新执行时机,避免不必要的重启。
- 使用 sealed class 表示 UI 状态:让状态变化更清晰、可预测。
- 在 DisposableEffect 中始终提供 onDispose:防止资源泄漏。
- 优先使用 LaunchedEffect 而非 GlobalScope:确保协程与组件生命周期绑定。
7.2 常见陷阱
| 陷阱 | 错误示例 | 正确做法 |
|---|---|---|
| 在 Composable 中直接启动协程 | CoroutineScope(Dispatchers.IO).launch { ... } | 使用 LaunchedEffect 或 rememberCoroutineScope |
| 忘记清理资源 | DisposableEffect(Unit) { registerListener() } | 始终提供 onDispose 块 |
| 使用不当的 key | LaunchedEffect(Unit) { loadData() } 但数据源会变化 | 将依赖状态作为 key |
| 在 SideEffect 中修改状态 | SideEffect { count++ } | 可能导致无限循环 |
| 忽略异常处理 | LaunchedEffect(key) { val data = fetch() } | 使用 try-catch 捕获异常 |
八、完整示例:综合应用
@Composable
fun WeatherApp(cityId: String) {
var weatherData by remember { mutableStateOf<Weather?>(null) }
var isLoading by remember { mutableStateOf(true) }
var errorMessage by remember { mutableStateOf<String?>(null) }
// 1. LaunchedEffect:网络请求
LaunchedEffect(cityId) {
isLoading = true
try {
weatherData = fetchWeather(cityId)
errorMessage = null
} catch (e: Exception) {
errorMessage = "加载失败: ${e.message}"
} finally {
isLoading = false
}
}
// 2. SideEffect:页面访问埋点
SideEffect {
Analytics.trackScreen("WeatherDetail", mapOf("cityId" to cityId))
}
// 3. DisposableEffect:注册/反注册网络状态监听
DisposableEffect(Unit) {
val listener = NetworkChangeListener { isConnected ->
if (!isConnected) errorMessage = "网络连接已断开"
}
NetworkManager.register(listener)
onDispose {
NetworkManager.unregister(listener)
}
}
// UI 渲染
when {
isLoading -> CircularProgressIndicator()
errorMessage != null -> Text(errorMessage!!, color = Color.Red)
weatherData != null -> WeatherDisplay(weatherData!!)
}
}
九、总结
| 概念 | 核心要点 |
|---|---|
| 副作用 | 可组合函数外部的操作,如网络请求、监听器、定时器 |
| LaunchedEffect | 启动协程,与组件生命周期绑定,key 变化时重启 |
| DisposableEffect | 资源管理,必须提供 onDispose 清理资源 |
| SideEffect | 每次重组后执行,用于同步外部状态 |
| rememberCoroutineScope | 获取协程作用域,用于事件回调 |
| produceState | 将外部状态源转换为 Compose State |
| derivedStateOf | 创建派生状态,避免不必要的重组 |
核心原则:
- ✅ 副作用必须封装在 Effect API 中
- ✅ 合理使用 key 控制执行时机
- ✅ 始终清理资源,避免内存泄漏
- ✅ 使用 sealed class 管理 UI 状态
十、线上资料链接
官方文档
- Compose 副作用官方指南:https://developer.android.com/jetpack/compose/side-effects
- LaunchedEffect 官方文档:https://developer.android.com/reference/kotlin/androidx/compose/runtime/LaunchedEffect
- DisposableEffect 官方文档:https://developer.android.com/reference/kotlin/androidx/compose/runtime/DisposableEffect
- SideEffect 官方文档:https://developer.android.com/reference/kotlin/androidx/compose/runtime/SideEffect
优质文章
- Jetpack Compose State Management: From Basics to Advanced Patterns:https://www.iflair.com/jetpack-compose-state-management-from-basics-to-advanced-patterns/
- Compose 状态管理完整教程:https://cnb.cool/cool-cc/learn-jetpack-compose/-/blob/main/lesson-03-state-management/README.md
通过掌握副作用处理,你将能够安全地在 Compose 应用中执行网络请求、定时器、监听器等操作,构建健壮、可维护的应用程序。
更多推荐



所有评论(0)