Android Compose SideEffect(副作用)实例加倍详解

前几天的文章,整理了上个月的学习心得。这段时间又仔细研究了下SideEffect,秉着闲着找事干,有事干才不想着去洗脚的原则,我又开始了读书笔记式的码字。源码在文章末尾。各副作用函数特性表也在文章末尾
部分例子在实际开发中是可能存在一定瑕疵的,旨在让大家明白如何使用。使用时请注意

一、副作用到底有哪些?

  • LaunchedEffect
  • DisposableEffect
  • SideEffect
  • rememberCoroutineScope
  • produceState
    副作用函数关联概念(个人觉得不是副函数):
  • derivedStateOf
  • rememberUpdatedState

上篇文章我已经简单介绍了这几个副作用函数,可总觉得太过简略,例子也很简单,完全不贴合实际开发,以后我怎么抄代码嘛。好的,下面我们就从实际出发举例子,让大家更好抄更好理解。

LaunchedEffect

1、概括

LaunchedEffect(key) = 页面进入时自动启动协程,key变了就取消旧的,重启新的。

2、核心机制

机制 说明
自动启动 Composable 进入组合时,自动 launch 协程
key 控制 key 不变 → 协程继续;key 变 → 取消旧协程,启动新协程
自动取消 离开页面 → 自动 cancel()零泄漏

3、实际开发中的场景

  • 场景一:加载网络数据(最常见)
kotlin 复制代码
@Composable
fun FetchUserInfo(userId: Int) {
    var user by remember { mutableStateOf<User?>(null) }
    var loading by remember { mutableStateOf(true) }
    var error by remember { mutableStateOf<String?>(null) }

    LaunchedEffect(userId) {  // key = userId
        loading = true
        error = null
        try {
            val result = withContext(Dispatchers.IO) {
                RetrofitClient.apiService.getUsersById(userId) 
            }
            user = result
        } catch (e: Exception) {
            error = e.message
        } finally {
            loading = false
        }
    }

    when {
        loading -> CircularProgressIndicator()
        error != null -> Text("错误: $error", color = Color.Red)
        user != null -> Column {
            Text("用户名: ${user!!.name}", style = MaterialTheme.typography.bodyLarge)
            Text("邮箱: ${user!!.email}")
        }
    }
}

代码核心:切换 userId → 自动取消旧请求,发起新请求

  • 场景二:一次性任务初始化(日志、埋点、第三方初始化)
kotlin 复制代码
@Composable
fun AnalyticsScreen(screenName: String) {
    LaunchedEffect(Unit) {  // 只执行一次
        //Analytics.uploadPoint(screenName)
        Log.i("当前页面: $screenName")
    }

    Text("当前页面: $screenName")
}

为什么使用Unit?Unit在Kotlin里面是常量,意味着key永远不变 ,所以只在页面首次进入时执行一次。

小知识:LaunchedEffect(true)和LaunchedEffect(Unit)行为一致,因为 true 也是常量。但 true容易让人误解为"每次都执行",建议统一用 Unit。

  • 场景三:轮询(实时获取数据)
kotlin 复制代码
@Composable
fun GoldPrice(symbol: String) {
    var price by remember { mutableStateOf<Price?>(null) }
    LaunchedEffect(symbol) {
        while (true) {
            price = withContext(Dispatchers.IO) {
                GoldPriceRetrofitClient.apiService.getGoldPrice(symbol)
            }
            delay(30_000)
        }
    }
    Card(
        modifier = Modifier.padding(16.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(symbol, style = MaterialTheme.typography.bodyMedium)
            Text("¥${price?.price?:"未知"}", style = MaterialTheme.typography.bodyLarge)
        }
    }
}

代码核心:每30秒获取一次金价。调整商品编码时,自动停止旧商品轮询,开始新轮询。

  • 场景四:执行一个协程
kotlin 复制代码
@Composable
fun LoadData(viewModel: SideEffectsViewModel = viewModel()) {
    var user by remember { mutableStateOf<User?>( null) }
    LaunchedEffect(Unit) {
        user = viewModel.getUserInfo(1)
    }
}

代码核心:执行获取用户查询的supend的方法

4、快速记忆

进入页 → 自动启动 → key变化自动重启 → 离开自动取消

5、常犯错误

错误 后果 正确
LaunchedEffect(true) 易产生语义歧义 Unit
不用 withContext(IO) 阻塞 UI 必须切线程
忘记 key 数据不更新 必须加依赖参数

6、总结

LaunchedEffect 是Compose中页面级异步任务的首选,key+自动取消是其核心。

DisposableEffect

1、概括

DisposableEffect(key) = 「进入时注册,离开时自动清理」的资源管理神器

2、核心机制

机制 说明
进入时执行 Composable 进入组合时,执行 effect
key 控制 key 变化 → onDispose,再重新注册
离开时自动清理 离开页面 → 自动调用 onDispose,防止内存泄漏

3、实际开发中场景

  • 场景一:注册/注销广播接收器(最常见)
kotlin 复制代码
@Composable
fun NetworkChangeListener(context: Context) {
    val connectivityManager = remember { 
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 
    }
    var isConnected by remember { mutableStateOf(true) }

    DisposableEffect(Unit) {
        val callback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                isConnected = true
            }
            override fun onLost(network: Network) {
                isConnected = false
            }
        }

        connectivityManager.registerDefaultNetworkCallback(callback)

        onDispose {
            connectivityManager.unregisterNetworkCallback(callback) // 必须清理!
        }
    }

    Card(
        colors = CardDefaults.cardColors(
            containerColor = if (isConnected) Color.Green else Color.Red
        )
    ) {
        Text(
            text = if (isConnected) "在线" else "离线",
            color = Color.White,
            modifier = Modifier.padding(8.dp)
        )
    }
}

代码核心:监听设备网络状态,断开网络显示离线,否则显示在线。

  • 场景二:添加移除传感器
kotlin 复制代码
@Composable
fun StepCounter(sensorManager: SensorManager) {
    var steps by remember { mutableStateOf(0) }

    DisposableEffect(Unit) {
        val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent) {
                steps = event.values[0].toInt()
            }
            override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
        }

        sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)

        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }

    Text("今日步数: $steps 步", style = MaterialTheme.typography.headlineMedium)
}

代码核心:实时监测运动传感器,统计步数。

  • 场景三:订阅取消Flow(ViewModel 搭配使用)
kotlin 复制代码
@Composable
fun FlowSubscription(viewModel: SideEffectsViewModel = viewModel()) {
    var message by remember { mutableStateOf("等待消息...") }

    DisposableEffect(Unit) {
        val job = viewModel.messageFlow
            .onEach { message = it }
            .launchIn(viewModel.viewModelScope)

        onDispose {
            job.cancel() // 取消订阅
        }
    }
    Column(modifier = Modifier.fillMaxSize()) {
        Button(onClick = {
            viewModel.sendMessage()
        }, modifier = Modifier.padding(16.dp)) {
            Text("点击发消息")
        }

        Text(message)
    }
}

代码核心:离开时,自动取消Flow

  • 场景四:key变化时,重置资源(如切换用户)
kotlin 复制代码
@Composable
fun UserSessionTracker(userId: String) {
    DisposableEffect(userId) {
        LoginManager.login(userId)
        println("用户 $userId 已登录")

        onDispose {
            LoginManager.logout()
            println("用户已登出")
        }
    }

    Text("当前用户: $userId")
}

代码核心:切换 userId → 自动登出旧用户,登录新用户

4、快速记忆

进入注册 → key变化重置 → 离开自动清理 → 安全无泄漏

5、常见错误

错误 后果 正确
忘记 onDispose 内存泄漏 必须清理
LaunchedEffect 注册监听 无法手动清理 DisposableEffect
key 写错 重复注册 用稳定 key

6、总结

DisposableEffect 是 Compose 中「资源生命周期管理」的唯一正确方式, 掌握 onDispose + key,解决内存泄漏的问题

SideEffect

1、一句话概括

SideEffect = 「每次重组都执行一次」的轻量副作用,不自动取消,适合日志、埋点、同步非 Compose 状态

2、核心机制

机制 说明
每次重组都执行 只要 UI 重组,SideEffect 就运行一次
无 key 控制 不支持 key不能重启或取消
不自动清理 离开页面不会自动取消,需手动控制

3、实际开发中的四大场景

  • 场景一:页面曝光埋点(常见)
kotlin 复制代码
@Composable
fun AnalyticsScreen(screenName: String) {
    SideEffect {
        Analytics.logScreenView(screenName)  // 每次重组都上报
        Log.i("compose 曝光","曝光: $screenName")
    }

    Text("当前页面: $screenName")
}
  • 场景二:日志输出及调试
kotlin 复制代码
@Composable
fun DebugRecomposition() {
    var count by remember { mutableStateOf(0) }

    SideEffect {
        Log.d("compose日志","重组发生了!count = $count")
    }

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) { Text("加1") }
    }
}
  • 场景三:同步或发送数据到外部(如SharedPreferences、EventBus、ViewModel)
kotlin 复制代码
@Composable
fun SyncTheme(isDark: Boolean) {
    val prefs = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
    SideEffect {
        prefs.edit().putBoolean("dark_mode", isDark).apply()
        Log.d("compose 日志", "主题已保存: ${if (isDark) "深色" else "浅色"}")
        EventBus.post(isDark)
    }

    // UI...
}
kotlin 复制代码
@Composable
fun BrightnessControl(viewModel: SideEffectsViewModel = viewModel()) {
    var brightness by remember { mutableFloatStateOf(0.5f) }

    // 每次 brightness 改变并重组完成后,同步到 ViewModel
    SideEffect {
        viewModel.currentBrightness = brightness
    }

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("当前亮度: ${(brightness * 100).toInt()}%")
        Slider(value = brightness, onValueChange = { brightness = it })
    }
}

核心:每次切换主题 → 自动保存到本地,并通知其他页面

  • 场景四:与非 Compose 组件通信(如 TextureView)
kotlin 复制代码
@Composable
fun CameraPreview(textureView: TextureView) {
    SideEffect {
        CameraManager.bind(textureView.surfaceTexture)
    }

    AndroidView(factory = { textureView })
}

4、快速记忆

重组就跑,轻量选它,日志埋点,首选SideEffect。

5、常见错误

错误 后果 正确
放网络请求 每次重组都请求 LaunchedEffect
放耗时操作(非轻量) 卡 UI withContext(IO) + LaunchedEffect
期望只执行一次 不会 LaunchedEffect(Unit)

总结:每次重组都会执行。注意:SideEffect 是在 重组完成后立即执行 ,没有协程。而 LaunchedEffect 是在进入组合时启动协程。

rememberCoroutineScope

1、一句话概括:

rememberCoroutineScope() = 「手动、可控、生命周期安全 」的协程作用域,重组不重启,离开页面自动取消

2、核心机制

机制 说明
手动启动 必须 scope.launch { } 才执行
重组不影响 重组时 同一个 scope,不会重新创建
自动取消 离开页面 → 自动 cancel() ,所有协程停止

3、常用场景

  • 场景一:异步操作(如文件保存,最常见)
kotlin 复制代码
@Composable
fun SaveButton(viewModel: SaveViewModel) {
    val scope = rememberCoroutineScope()
    var saving by remember { mutableStateOf(false) }

    Button(
        enabled = !saving,
        onClick = {
            saving = true
            scope.launch {
                try {
                    viewModel.saveData()  // 模拟文件保存
                } finally {
                    saving = false  // 发送错误时,重置UI
                }
            }
        }
    ) {
        Text(if (saving) "保存中..." else "保存")
    }
}

场景二:定时任务

kotlin 复制代码
@Composable
fun TimerWithCoroutineScope() {
    val scope = rememberCoroutineScope()
    var count by remember { mutableIntStateOf(0) }
    
    Button(onClick = {
        scope.launch {
            repeat(10) {
                delay(1000)
                count++
            }
        }
    }) {
        Text("开始定时任务")
    }
    
    Text("计数: $count")
}

场景三:批量处理

kotlin 复制代码
@Composable
fun BatchProcess(items: List<String>) {
    val scope = rememberCoroutineScope()
    var progress by remember { mutableStateOf(0) }

    Button(onClick = {
        scope.launch {
            items.forEachIndexed { index, item ->
                processItem(item)
                progress = index + 1
            }
        }
    }) {
        Text("处理 ${items.size} 项")
    }
    LinearProgressIndicator(progress = progress / items.size.toFloat())
}
  • 场景四:与Snackbar搭配(Material3 常见用法)
kotlin 复制代码
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SnackbarExample() {
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                scope.launch {
                    snackbarHostState.showSnackbar("Hello Snackbar!")
                }
            }) {
                Text("点我")
            }
        }
    ) { padding ->
        Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
            Text("点击右下角按钮显示 Snackbar")
        }
    }
}

4、快速记忆

rememberCoroutineScope → 手动 launch → 自动取消 → 重组不重启

它与 LaunchedEffect 最大的区别是:需要事件触发 而不是自动执行。

5、常见错误

错误 后果 正确
LaunchedEffect 放点击 每次重组都弹 scope.launch
不用 remember 每次重组新 scope 必须 rememberCoroutineScope()
GlobalScope 无法取消 禁止使用!

6、总结

rememberCoroutineScope 是 Compose 中「用户交互 → 异步操作」的最佳实践,需要手动启动协程,首选选他。

ProduceState

1、一句话概括

produceState = 「外部数据源 转化为 Compose State」的桥梁,自动协程 + 自动取消 + 支持 key

2、核心机制

机制 说明
协程生产 State 在协程中 value = xxx → 自动触发 UI 重组
自动生命周期 进入 → 启动协程;离开 → 自动取消
key 控制重启 key 变 → 取消旧协程,启动新协程
其更像是collectAsState()的增强版本,支持任意数据源,而collectAsState则只能来源于Flow。

常用场景

  • 场景一:请求网络数据(最常见)
kotlin 复制代码
@Composable
fun UserInfo(userId: Int) {
    val user by produceState<User?>(initialValue = null, userId) {
        value = withContext(Dispatchers.IO) {
            RetrofitClient.apiService.getUsersById(userId)  //根据userId获取用户数据
        }
    }

    when (user) {
        null -> CircularProgressIndicator()
        else -> Text("欢迎,${user!!.name}")
    }
}
  • 场景二:Flow -> State(较常见),替代collectAsState
kotlin 复制代码
class SideEffectsViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val countFlow = _count.asStateFlow()
    fun increment() = _count.value++
}

@Composable
fun FlowCounter(viewModel: SideEffectsViewModel = viewModel()) {
    //原写法
    // val count by viewModel.collectAsState()
    val count by produceState(initialValue = 0) {
        viewModel.countFlow.collect { value = it }
    }

    Text("Count: $count")
}
  • 场景三:回调函数-> State
kotlin 复制代码
@Composable
fun LocationNow(context: Context, locationManager: LocationManager) {
    val location by produceState<String?>(initialValue = null) {
        val listener = LocationListener { location -> value = "经度: ${location.longitude}, 纬度: ${location.latitude}" }
        //权限检查代码省略
        locationManager.requestLocationUpdates(
            LocationManager.GPS_PROVIDER,
            2000L,
            1f,
            listener)

        awaitDispose {
            locationManager.removeUpdates(listener)
        }
    }
    
    if (location == null) {
        Text("正在获取位置...")
    } else {
        Text("location")
    }
}
  • 场景四:状态处理
kotlin 复制代码
sealed class Result<out T> {
    object Loading : Result<Nothing>()
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>()
}

@Composable
fun <T> loadData(
    key: Any?,
    block: suspend () -> T
): State<Result<T>> = produceState<Result<T>>(Result.Loading, key) {
    try {
        value = Result.Success(block())
    } catch (e: Exception) {
        value = Result.Error(e.message ?: "未知错误")
    }
}

4、快速记忆

生产 State → 协程运行 → 自动取消 → key 控制重启

5、常见错误

错误 后果 正确
不用 withContext(IO) 阻塞 UI 必须切线程
忘记 key 数据不更新 加依赖参数
返回 State 编译错误 必须 by produceState

6、相似对比

项目 produceState collectAsState()
数据源 任意 仅 Flow
支持 key Yes No
自动取消 Yes Yes

7.总结:

produceState 是 Compose 中【外部数据源 -> State】的最好桥梁, 熟悉它,你就能实现任意数据源驱动UI变化了。

derivedStateOf

有人把这个归于副作用函数之类,但我个人觉得不是。原因如下:

  • 纯函数计算:基于其他状态计算的派送状态,不产生任何外部影响
  • 无外部交互:不执行I/O操作,不启动异步任务、不修改外部状态
  • 不直接参与UI更新:虽然在 Composable 中触发,但只是影响 UI 的"外部世界"

1、一句话概括:

derivedStateOf { } = 「只有当结果真正变化时才触发重组」,避免重复计算 + 减少 UI 闪烁

2、为什么需要derivedStateOf?

问题:频繁重组导致性能拉胯

kotlin 复制代码
@Composable
fun UserFilter(nameItems: List<String>, query: String) {
    val filtered = nameItems.filter { it.contains(query) }  // 每次重组都过滤!

    LazyColumn {
        nameItems(filtered) { Text(it) }
    }
}

这段代码你会发现query 变 → 重组 → 每次都重新 filter 。 即使结果没变,也会触发 LazyColumn 重绘 → 性能差 + 闪烁

相关改进参考场景一

3、常用场景

  • 场景一:搜索过滤
kotlin 复制代码
@Composable
fun SearchStr(nameItems: List<String>, query: String) {
    // 使用 derivedStateOf 只在 query 或 nameItems 变化时重新计算
    var searchQuery by remember { mutableStateOf(query) }
    val filteredItems by derivedStateOf {
        nameItems.filter { it.contains(searchQuery, ignoreCase = true) }
    }
    Column {
        TextField(value = searchQuery,
            onValueChange = { searchQuery = it },
            label = { Text("搜索") })
        LazyColumn {
            items(filteredItems) { item ->
                Text(item)
            }
        }
    }
}

代码核心:快速输入时 → 只有结果变了才更新列表,降低了重组次数

  • 场景二:分页加载判断
kotlin 复制代码
@Composable
fun InfiniteList(items: List<String>, onLoadMore: () -> Unit) {
    val listState = rememberLazyListState()
    
    // 只有到达底部时才触发
    val shouldLoadMore by derivedStateOf {
        val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
        lastVisible >= items.size - 2
    }

    LaunchedEffect(shouldLoadMore) {
        if (shouldLoadMore) onLoadMore()
    }

    LazyColumn(state = listState) {
        items(items) { Text(it) }
    }
}

代码核心:计算是否距离底部还有两个Item,是的话触发加载下一页回调操作。

  • 场景三:关联数据或状态实时计算(常见用途)
kotlin 复制代码
@Composable
fun  SumAndAvg(numbers: List<Int>) {
    val sum by remember {
        derivedStateOf {
            numbers.sum()
        }
    }
    val avg by remember {
        derivedStateOf {
            numbers.average()
        }
    }

    Card {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("总和: $sum")
            Text("平均: %.2f".format(avg))
            Text("Numbers 总数: ${numbers.size}")
        }
    }
}

-场景四:控件显示状态判断 (常见)

kotlin 复制代码
@Composable
fun TopButtonShow(listState: LazyListState) {
    val scope = rememberCoroutineScope()
    val showButton by remember {
        derivedStateOf {
            listState.firstVisibleItemIndex > 0
        }
    }

    AnimatedVisibility(showButton) {
        FloatingActionButton(onClick = {
            // 回到顶部
            scope.launch { 
                listState.animateScrollToItem(0)
            }
        }, modifier = Modifier.padding(16.dp)) {
            Icon(Icons.Default.KeyboardArrowUp, contentDescription = "回到顶部")
        }
    }
}

4、快速记忆

数据派生 -> 重组少 -> 性能优 -> 体验棒

5、常见错误

错误 后果 正确
不用 derivedStateOf 频繁重组 必须用
LaunchedEffect 无法驱动 UI by derivedStateOf
计算太重 卡 UI 移到 IO 线程

6、总结

derivedStateOf 是 Compose 中「计算优化 + 防闪烁」的最优解之一,掌握它能让你的UI更流畅。

rememberUpdatedState

我同样认为这不属于副作用函数,理由与derivedStateOf类似。但它与副作用LaunchedEffect函数配合紧密,所以一起介绍了。其实这里面涉及到一个闭包的概念。这里面就不展开讲,不了解的小伙伴去问AI吧。

1、一句话概括

rememberUpdatedState(value) = 「让 LaunchedEffect 里的协程,始终拿到最新的值」,即使它在重组时被捕获了旧值。

2、解决了什么问题

先看问题代码:

kotlin 复制代码
@Composable
fun GreetingMessage(message: String) {
    // 启动一个协程,每隔2秒打印一次 message
    LaunchedEffect(Unit) {
        while (true) {
            delay(2000)
            Log.d("message的值","当前 message: $message")  //  闭包捕获旧值
        }
    }
}

存在的问题:当 message 改变时,LaunchedEffect(Unit) 不会重启,因为key未发生变化; 因此协程里打印的仍然是"第一次 Composition"时的旧值; 这就叫「stale capture(过期捕获) 」。

解决方案:使用rememberUpdatedState进行修正

kotlin 复制代码
@Composable
fun UpdateNewMessage(message: String) {
    // 让协程中总能访问最新 message
    val currentMessage by rememberUpdatedState(message)

    LaunchedEffect(Unit) {
        while (true) {
            delay(2000)
            println("当前 message: $currentMessage")  // ✅ 始终是最新值
        }
    }
}

3、应用场景

  • 场景一:防抖(推荐此方式实现Compose的防抖)
kotlin 复制代码
@Composable
fun DebouncedSearch(
    query: String,
    onDebouncedQueryChange: (String) -> Unit
) {
    var searchQuery by remember { mutableStateOf(query) }
    val currentOnDebouncedQueryChange by rememberUpdatedState(onDebouncedQueryChange)

    LaunchedEffect(searchQuery) {
        delay(300) // 300ms 防抖延迟
        if(searchQuery.isNotBlank()) {
            currentOnDebouncedQueryChange(query)
        }

    }
    TextField( value = searchQuery,
        onValueChange = { searchQuery = it },
        label = { Text("搜索") } )
}
  • 场景二:事件监听中保持回调更新(最常见)
kotlin 复制代码
@Composable
fun LocationChange(onLocationUpdate: (Location) -> Unit) {
    val currentCallback by rememberUpdatedState(onLocationUpdate)
    
    DisposableEffect(Unit) {
        val listener = LocationListener { location ->
            currentCallback(location) // 使用最新的回调
        }
        
        locationManager.requestLocationUpdates(listener)
        
        onDispose {
            locationManager.removeUpdates(listener)
        }
    }
}

代码核心:时刻保持最新的回调,解决持有旧回调的问题

  • 场景三:定时器动态轮询(根据情况调整轮询时间)
kotlin 复制代码
/**
 * 每次调用调整轮询间隔,不触发重启
 */
@Composable
fun PollingData(intervalMs: Long) {
    val updatedInterval by rememberUpdatedState(intervalMs)
    var data by remember { mutableStateOf("加载中...") }

    LaunchedEffect(Unit) {
        while (true) {
            data = withContext(Dispatchers.IO) {
                RetrofitClient.apiService.getUsersById(1).toString()
            }
            delay(updatedInterval)  // 动态间隔
        }
    }

    Column {
        Text("数据: $data")
        Text("轮询间隔: $updatedInterval ms")
    }
}
  • 场景四:延迟提示(优化部分情况下,控件多次显示隐藏导致的闪烁)
kotlin 复制代码
@Composable
fun DelayedTooltip(visible: Boolean, text: String) {
    val updatedVisible by rememberUpdatedState(visible)

    LaunchedEffect(Unit) {
        delay(1000)  // 延迟显示
        if (updatedVisible) {
            showTooltip(text)
        }
    }

    // UI...
}

4、快速记忆

协程不重启,值要最新值,那就使用UpdatedState
LaunchedEffect(Unit) + rememberUpdatedState(value) = 不重启协程也能拿到最新值

5、常犯错误

错误 后果 正确
不用 rememberUpdatedState 协程用旧值 必须用
配合 LaunchedEffect(value) 频繁重启 Unit
SideEffect 每次重组都执行 错!

六、总结

rememberUpdatedState 是 Compose 中「协程 + 动态参数」的黄金搭档,经常与LaunchedEffect(Unit)搭配实现多场景功能。

总结:

Compose 的副作用函数本质是:在声明式UI中执行命令式逻辑的入口
LaunchedEffectDisposableEffectSideEffect 解决不同生命周期阶段的副作用;

rememberCoroutineScopeproduceStatederivedStateOfrememberUpdatedState 则是围绕状态、协程与性能优化的工具。理解它们的生命周期、触发时机与取消机制,是写好 Compose 的关键。

本文只列出了我能想到的一些场景,肯定有很多的遗漏,还是需要大家在使用中去完善了解

各副作用函数机制速查表:

函数 启动时机 重组触发 自动取消(随生命周期销毁) 常见用途 是否可持久运行(如轮询) 是否依赖 key
SideEffect 每次成功重组后执行 ✅ 每次重组都会执行 ❌ 不会自动取消 把 Compose 状态同步到非 Compose 系统(如 Log、ViewModel 状态等)
LaunchedEffect(key) 进入 Composition 时执行一次 ✅ key 变化会重启协程 ✅ key 改变或退出 Composition 时自动取消 执行一次性任务、启动协程、监听 Flow
DisposableEffect(key) 进入 Composition 时执行一次 ✅ key 变化或离开 Composition 时触发 onDispose ✅ 自动触发 onDispose 注册/注销监听、启动/停止外部资源
rememberCoroutineScope() 组合时创建一个可记忆的协程作用域 ❌ 不会因重组重新创建 ✅ Composition 离开时自动取消 需要在回调中手动启动协程(如按钮点击)
produceState() 进入 Composition 时启动协程 ✅ key 变化会重启生产逻辑 ✅ 自动取消旧协程 把异步任务结果转为 State(如网络请求)
derivedStateOf() 依赖 State 变化时计算 ✅ 依赖的值变化才重新计算 ❌(纯计算,此处无意义) 优化计算型状态,避免不必要重组
rememberUpdatedState() 每次重组更新内部值 ❌ 不会导致重组 让长期副作用中访问最新参数(如协程/回调)
snapshotFlow { } 在协程中把 State 转换为 Flow ✅ State 变化时发射新值 ✅ 协程取消即停止 响应式收集 State 变化

源码

github.com/tcyang12345...

相关推荐
火柴就是我3 小时前
mmkv的 mmap 的理解
android
没有了遇见3 小时前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong3 小时前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强4 小时前
如何简单 hack agp 执行过程中的某个类
android
沐怡旸4 小时前
【底层机制】 Android ION内存分配器深度解析
android·面试
你听得到114 小时前
肝了半个月,我用 Flutter 写了个功能强大的图片编辑器,告别image_cropper
android·前端·flutter
KevinWang_4 小时前
Android 原生 app 和 WebView 如何交互?
android
用户69371750013844 小时前
Android Studio中Gradle、AGP、Java 版本关系:不再被构建折磨!
android·android studio
杨筱毅5 小时前
【底层机制】Android低内存管理机制深度解析
android·底层机制