Jetpack Compose 入门系列(八):Compose 中的副作用处理

Jetpack Compose 入门系列(八):Compose 中的副作用处理

学完上篇你已经会用 ViewModel 和 UiState 管理页面状态了。但真实页面里不只有"根据状态画 UI",还会遇到倒计时、弹 Snackbar、监听生命周期、状态变化后执行一次任务这些场景,本篇解决一个问题:如何在 Compose 中正确处理副作用


一、什么是副作用

先别被"副作用"这个词吓到,它不是什么玄学。

在 Compose 里,Composable 最理想的状态是:给它什么状态,它就画什么 UI

比如:

kotlin 复制代码
@Composable
fun Greeting(
    name: String
) {
    Text(text = "你好,$name")
}

这段代码很纯粹:传入 name,显示文字,没有偷偷做别的事。

但真实开发里,经常会有这些需求:

  • 页面第一次进入时加载数据
  • 输入框内容变化后延迟搜索
  • 点击按钮弹 Snackbar
  • 页面进入前台时刷新数据
  • 页面离开时取消监听
  • 倒计时结束后执行回调

这些都不是"画 UI"本身,而是在 UI 之外额外做的事情,所以叫副作用。

来做个对比你就明白了:

行为 是不是副作用 原因
显示一段文字 不是 只是根据状态画 UI
在 Composable 函数体执行期间直接修改 MutableState 是,而且通常是错误写法 可能导致重复重组或不可预测行为
在点击、输入等事件回调里修改状态 通常不是问题 这是正常的状态更新
启动协程请求数据 发生在 UI 绘制之外
注册生命周期监听 需要创建和清理外部资源
弹 Snackbar 是一次性事件,不是静态 UI

在 XML 时代,这些事情通常写在 ActivityFragment 的生命周期方法里:

kotlin 复制代码
override fun onStart() {
    super.onStart()
    startLocationUpdates()
}

override fun onStop() {
    stopLocationUpdates()
    super.onStop()
}

到了 Compose 里,没有 onStart() 这种直接写在 Composable 里的生命周期方法。Compose 提供了一组和副作用、状态派生、重组控制相关的常用 API:

API 适合场景 类型
LaunchedEffect 启动协程、状态变化后执行挂起任务 副作用
rememberCoroutineScope 在点击事件中手动启动协程 副作用辅助
DisposableEffect 注册监听,并在离开时清理 副作用
rememberUpdatedState 在长时间运行的 Effect 中拿到最新回调 Effect 辅助
derivedStateOf 根据已有状态计算派生状态,减少不必要重组 派生状态

关键点:Composable 负责描述 UI,副作用相关 API 负责处理那些"不能直接写在 UI 描述里"的事情;派生状态工具负责把能计算出来的状态留在状态系统里。


二、添加依赖

这篇主要用 Compose、Material3 和 Lifecycle。

2.1 libs.versions.toml

toml 复制代码
[versions]
agp = "9.2.1"
kotlin = "2.2.10"
composeBom = "2026.02.01"
activity = "1.8.0"
lifecycle = "2.11.0"

[libraries]
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

2.2 app/build.gradle.kts

kotlin 复制代码
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.compose.compiler)
}

android {
    namespace = "com.example.sideeffectdemo"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.sideeffectdemo"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"
    }

    buildFeatures {
        compose = true
    }
}

dependencies {
    implementation(platform(libs.androidx.compose.bom))

    implementation(libs.androidx.activity.compose)
    implementation(libs.androidx.compose.material3)
    implementation(libs.androidx.compose.ui.tooling.preview)
    implementation(libs.androidx.lifecycle.runtime.compose)
}

这段配置里发生了什么:

  1. activity-compose 用来在 Activity 中启动 Compose。
  2. material3 提供 ScaffoldSnackbarHostButtonOutlinedTextField 等组件。
  3. lifecycle-runtime-compose 提供 Compose 和生命周期结合时需要的能力。

本篇的重点不是依赖,而是理解副作用 API 的使用边界:什么时候用、为什么用、怎么避免重复执行。


三、LaunchedEffect:进入组合后启动协程

LaunchedEffect 是最常用的副作用 API。

它适合做这些事:

  • 页面第一次显示时执行一次任务
  • 某个 key 变化后重新执行任务
  • 在 Composable 生命周期内启动协程
  • key 变化或离开组合时自动取消旧任务

先看一个简单例子:输入用户名后,延迟 500ms 检查长度。

kotlin 复制代码
@Composable
private fun UsernameChecker() {
    var username by rememberSaveable { mutableStateOf("") }
    var message by rememberSaveable { mutableStateOf("请输入用户名") }

    LaunchedEffect(username) {
        if (username.isBlank()) {
            message = "请输入用户名"
            return@LaunchedEffect
        }

        delay(500)

        message = if (username.length >= 3) {
            "用户名可用"
        } else {
            "用户名至少需要 3 个字符"
        }
    }

    Column(
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        OutlinedTextField(
            value = username,
            onValueChange = {
                username = it
            },
            label = {
                Text(text = "用户名")
            }
        )

        Text(text = message)
    }
}

这段代码里发生了什么:

  1. username 是输入框状态。
  2. LaunchedEffect(username) 表示:当 username 变化时,执行里面的协程。
  3. 如果用户名为空,直接显示提示。
  4. 如果不为空,先 delay(500),再检查长度。
  5. 如果用户在 500ms 内继续输入,旧的协程会被取消,新的协程重新开始。

这正好适合"输入后延迟搜索"这种场景。

sequenceDiagram participant U as 用户输入 participant S as username 状态 participant E as LaunchedEffect(username) participant UI as UI U->>S: 输入 a S->>E: 启动检查任务 U->>S: 继续输入 ab S->>E: 取消旧任务,启动新任务 U->>S: 继续输入 abc S->>E: 再次取消旧任务,启动新任务 E->>UI: 500ms 后显示"用户名可用"

LaunchedEffect(key) 的 key 很重要:key 不变,任务不会重启;key 变化,旧任务取消,新任务启动。

3.1 常见错误:不要把协程直接写在 Composable 函数体里

❌ 错误:

kotlin 复制代码
@Composable
fun BadScreen() {
    var text by rememberSaveable { mutableStateOf("") }

    CoroutineScope(Dispatchers.Main).launch {
        delay(1000)
        text = "加载完成"
    }

    Text(text = text)
}

这段代码的问题很大:Composable 可能会重组很多次,每次重组都可能新建协程,最后你会得到一堆失控任务。

✅ 正确:

kotlin 复制代码
@Composable
fun GoodScreen() {
    var text by rememberSaveable { mutableStateOf("") }

    LaunchedEffect(Unit) {
        delay(1000)
        text = "加载完成"
    }

    Text(text = text)
}

LaunchedEffect(Unit) 表示这个任务在当前 Composable 进入组合后执行一次。

只要你想在 Composable 中启动协程,第一反应应该是:这个任务是否适合放进 LaunchedEffect


四、rememberCoroutineScope:在事件里启动协程

LaunchedEffect 适合"状态变化后自动执行"。

但有些任务是用户主动触发的,比如点击按钮弹 Snackbar。

这时可以用 rememberCoroutineScope()

kotlin 复制代码
@Composable
private fun SnackbarDemo() {
    val snackbarHostState = remember { SnackbarHostState() }
    val coroutineScope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { innerPadding ->
        Button(
            modifier = Modifier.padding(innerPadding),
            onClick = {
                coroutineScope.launch {
                    snackbarHostState.showSnackbar("保存成功")
                }
            }
        ) {
            Text(text = "保存")
        }
    }
}

这段代码里发生了什么:

  1. SnackbarHostState 负责管理 Snackbar。
  2. rememberCoroutineScope() 创建一个和当前组合绑定的协程作用域。
  3. 用户点击按钮时,调用 coroutineScope.launch
  4. showSnackbar() 是挂起函数,所以需要在协程里调用。
  5. 当前 Composable 离开组合时,这个 scope 里的任务也会被取消。

LaunchedEffectrememberCoroutineScope 怎么选?

场景 推荐 API
页面进入后自动加载 LaunchedEffect(Unit)
某个状态变化后执行任务 LaunchedEffect(key)
点击按钮后弹 Snackbar rememberCoroutineScope()
点击按钮后执行挂起函数 rememberCoroutineScope()

简单记:自动发生用 LaunchedEffect,用户事件里手动触发用 rememberCoroutineScope


五、DisposableEffect:注册监听,并负责清理

有些副作用不是执行一次就完了,而是"注册一个东西,然后离开时要清掉"。

比如监听生命周期:

kotlin 复制代码
@Composable
private fun LifecycleStatusText() {
    val lifecycleOwner = LocalLifecycleOwner.current
    var lifecycleText by rememberSaveable { mutableStateOf("等待生命周期事件") }

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            lifecycleText = "当前生命周期事件:$event"
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    Text(text = lifecycleText)
}

需要的 import:

kotlin 复制代码
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner

这段代码里发生了什么:

  1. LocalLifecycleOwner.current 拿到当前页面的生命周期持有者。
  2. DisposableEffect(lifecycleOwner) 表示当 lifecycleOwner 变化时重新注册监听。
  3. LifecycleEventObserver 监听生命周期事件。
  4. addObserver(observer) 注册监听。
  5. onDispose 中调用 removeObserver(observer) 清理监听。

如果你只注册不清理,就像进房间开了灯,走的时候不关灯。短时间看不出问题,时间长了就容易泄漏。

DisposableEffect 的重点不只是"执行副作用",而是必须在 onDispose 里清理资源。

5.1 常见错误:注册监听但不移除

❌ 错误:

kotlin 复制代码
@Composable
fun BadLifecycleScreen() {
    val lifecycleOwner = LocalLifecycleOwner.current

    LaunchedEffect(Unit) {
        lifecycleOwner.lifecycle.addObserver(
            LifecycleEventObserver { _, event ->
                println(event)
            }
        )
    }
}

这段代码没有移除监听,页面离开后 observer 可能还在。

✅ 正确:

kotlin 复制代码
@Composable
fun GoodLifecycleScreen() {
    val lifecycleOwner = LocalLifecycleOwner.current

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            println(event)
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

只要你的副作用需要"成对出现"的注册和注销,就优先考虑 DisposableEffect


六、rememberUpdatedState:在长任务中拿到最新值

rememberUpdatedState 是初学时最容易忽略,但实际很有用的 API。

先看一个倒计时组件:

kotlin 复制代码
@Composable
private fun CountdownText(
    seconds: Int,
    onFinished: () -> Unit
) {
    var remainingSeconds by rememberSaveable(seconds) {
        mutableIntStateOf(seconds)
    }

    LaunchedEffect(seconds) {
        remainingSeconds = seconds

        while (remainingSeconds > 0) {
            delay(1000)
            remainingSeconds--
        }

        onFinished()
    }

    Text(text = "剩余时间:$remainingSeconds 秒")
}

这段代码能跑,但有个隐藏问题:如果倒计时过程中外层传进来的 onFinished 变了,LaunchedEffect 里的协程可能还拿着旧的回调。

更稳妥的写法是:

kotlin 复制代码
@Composable
private fun CountdownText(
    seconds: Int,
    onFinished: () -> Unit
) {
    val latestOnFinished by rememberUpdatedState(onFinished)
    var remainingSeconds by rememberSaveable(seconds) {
        mutableIntStateOf(seconds)
    }

    LaunchedEffect(seconds) {
        remainingSeconds = seconds

        while (remainingSeconds > 0) {
            delay(1000)
            remainingSeconds--
        }

        latestOnFinished()
    }

    Text(text = "剩余时间:$remainingSeconds 秒")
}

这段代码里发生了什么:

  1. rememberUpdatedState(onFinished) 保存最新的 onFinished
  2. LaunchedEffect(seconds) 只跟 seconds 绑定。
  3. 倒计时过程中,如果外层回调变化,不会重启倒计时。
  4. 倒计时结束时,调用的是最新的 onFinished

为什么不直接把 onFinished 放进 LaunchedEffect 的 key?

kotlin 复制代码
LaunchedEffect(seconds, onFinished) {
    // 倒计时
}

这样只要回调引用变化,倒计时就会重启。很多时候这不是你想要的结果。

rememberUpdatedState 适合解决一个问题:Effect 不想重启,但里面又想拿到最新的外部值或回调。


七、derivedStateOf:从已有状态计算派生状态

有些状态不需要单独保存,而是可以从已有状态算出来。

比如注册按钮是否可点:

  • 用户名至少 3 个字符
  • 密码至少 6 个字符
  • 没有正在提交

很多人会这样写:

kotlin 复制代码
var canSubmit by rememberSaveable { mutableStateOf(false) }

然后在用户名变化、密码变化、提交状态变化时到处改 canSubmit

这就容易漏。

更推荐用 derivedStateOf

kotlin 复制代码
@Composable
private fun RegisterForm() {
    var username by rememberSaveable { mutableStateOf("") }
    var password by rememberSaveable { mutableStateOf("") }
    var isSubmitting by rememberSaveable { mutableStateOf(false) }

    val canSubmit by remember {
        derivedStateOf {
            username.length >= 3 && password.length >= 6 && !isSubmitting
        }
    }

    Column(
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        OutlinedTextField(
            value = username,
            onValueChange = {
                username = it
            },
            label = {
                Text(text = "用户名")
            }
        )

        OutlinedTextField(
            value = password,
            onValueChange = {
                password = it
            },
            label = {
                Text(text = "密码")
            }
        )

        Button(
            enabled = canSubmit,
            onClick = {
                isSubmitting = true
            }
        ) {
            Text(text = if (isSubmitting) "提交中" else "注册")
        }
    }
}

这段代码里发生了什么:

  1. usernamepasswordisSubmitting 是原始状态。
  2. canSubmit 是从原始状态计算出来的派生状态。
  3. 任意原始状态变化后,canSubmit 会重新计算。
  4. 不需要手动维护 canSubmit

不过要注意:derivedStateOf 不是副作用,它只是派生状态工具。也不是所有计算值都必须用 derivedStateOf

如果只是非常简单的计算,直接写普通 val 也可以:

kotlin 复制代码
val canSubmit = username.length >= 3 && password.length >= 6 && !isSubmitting

derivedStateOf 更适合这些场景:原始状态变化很频繁,但派生结果不一定每次都变化,或者计算本身有一定成本。比如列表滚动位置频繁变化,但你只关心"是否已经滚过第一项":

kotlin 复制代码
val showBackToTop by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

能从已有状态算出来的值,就不要再额外保存一份。多保存一份,就多一个不同步的风险;是否需要 derivedStateOf,则要看它能不能减少不必要的重组或重复计算。


八、综合实战:注册页面副作用 Demo

现在把前面的知识串起来,做一个注册页面:

  • 用户名输入后延迟校验
  • 密码输入后计算按钮是否可用
  • 点击注册后弹 Snackbar
  • 注册成功后启动倒计时
  • 倒计时结束后更新提示
  • 页面监听生命周期事件

这个 Demo 会覆盖:

  • LaunchedEffect
  • rememberCoroutineScope
  • DisposableEffect
  • rememberUpdatedState
  • derivedStateOf

说明:这个 Demo 为了集中展示多个 API,把状态和逻辑都放在同一个页面里。真实项目中,业务状态通常会放到 ViewModel,Composable 主要负责展示状态和分发事件。

完整代码如下:

kotlin 复制代码
package com.example.sideeffectdemo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                RegisterSideEffectDemo()
            }
        }
    }
}

@Composable
private fun RegisterSideEffectDemo() {
    val snackbarHostState = remember { SnackbarHostState() }
    val coroutineScope = rememberCoroutineScope()

    var username by rememberSaveable { mutableStateOf("") }
    var password by rememberSaveable { mutableStateOf("") }
    var usernameMessage by rememberSaveable { mutableStateOf("请输入用户名") }
    var isSubmitting by rememberSaveable { mutableStateOf(false) }
    var showCountdown by rememberSaveable { mutableStateOf(false) }
    var resultMessage by rememberSaveable { mutableStateOf("等待注册") }

    val canSubmit by remember {
        derivedStateOf {
            username.length >= 3 && password.length >= 6 && !isSubmitting
        }
    }

    LaunchedEffect(username) {
        if (username.isBlank()) {
            usernameMessage = "请输入用户名"
            return@LaunchedEffect
        }

        usernameMessage = "正在检查用户名......"
        delay(500)
        usernameMessage = if (username.length >= 3) {
            "用户名格式正确"
        } else {
            "用户名至少需要 3 个字符"
        }
    }

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(24.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Text(
                text = "注册页面",
                style = MaterialTheme.typography.headlineMedium
            )

            LifecycleStatusText()

            OutlinedTextField(
                value = username,
                onValueChange = {
                    username = it
                },
                modifier = Modifier.fillMaxWidth(),
                label = {
                    Text(text = "用户名")
                }
            )

            Text(text = usernameMessage)

            OutlinedTextField(
                value = password,
                onValueChange = {
                    password = it
                },
                modifier = Modifier.fillMaxWidth(),
                label = {
                    Text(text = "密码")
                }
            )

            Button(
                enabled = canSubmit,
                onClick = {
                    isSubmitting = true
                    resultMessage = "注册成功,倒计时结束后进入下一步"
                    showCountdown = true

                    coroutineScope.launch {
                        snackbarHostState.showSnackbar("注册成功")
                    }
                }
            ) {
                Text(text = if (isSubmitting) "已提交" else "注册")
            }

            Text(text = resultMessage)

            if (showCountdown) {
                CountdownText(
                    seconds = 5,
                    onFinished = {
                        showCountdown = false
                        isSubmitting = false
                        resultMessage = "倒计时结束,可以进入下一步"
                    }
                )
            }
        }
    }
}

@Composable
private fun LifecycleStatusText() {
    val lifecycleOwner = LocalLifecycleOwner.current
    var lifecycleText by rememberSaveable { mutableStateOf("等待生命周期事件") }

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            lifecycleText = "生命周期事件:$event"
        }

        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    Text(text = lifecycleText)
}

@Composable
private fun CountdownText(
    seconds: Int,
    onFinished: () -> Unit
) {
    val latestOnFinished by rememberUpdatedState(onFinished)
    var remainingSeconds by rememberSaveable(seconds) {
        mutableIntStateOf(seconds)
    }

    LaunchedEffect(seconds) {
        remainingSeconds = seconds

        while (remainingSeconds > 0) {
            delay(1000)
            remainingSeconds--
        }

        latestOnFinished()
    }

    Text(text = "剩余时间:$remainingSeconds 秒")
}

这份 Demo 的数据流如下:

flowchart TD A[输入用户名] --> B[username 改变] B --> C[LaunchedEffect username 重启] C --> D[延迟 500ms 校验] D --> E[更新 usernameMessage] F[点击注册] --> G[rememberCoroutineScope 启动协程] G --> H[弹 Snackbar] F --> I[显示 CountdownText] I --> J[LaunchedEffect seconds 启动倒计时] J --> K[倒计时结束调用最新 onFinished] L[页面进入/离开] --> M[DisposableEffect 注册/清理生命周期监听]

实战知识点对应表

实战中的效果 使用的 API 对应章节
用户名输入后延迟校验 LaunchedEffect(username)
点击注册后弹 Snackbar rememberCoroutineScope()
监听生命周期事件 DisposableEffect(lifecycleOwner)
倒计时结束调用最新回调 rememberUpdatedState(onFinished)
注册按钮是否可点 derivedStateOf
状态保存 rememberSaveable 三、八

这就是副作用处理的核心思路:UI 仍然负责展示,副作用 API 负责把"额外要做的事"放到正确的生命周期里。


九、副作用与相关状态工具速查表

API 什么时候用 类型 / 关键点
LaunchedEffect(Unit) 进入页面执行一次挂起任务 副作用;当前 Composable 离开时自动取消
LaunchedEffect(key) key 变化后重新执行任务 副作用;key 变化会取消旧任务
rememberCoroutineScope() 点击事件中手动启动协程 副作用辅助;适合 Snackbar、点击后调用挂起函数
DisposableEffect(key) 注册监听、广播、观察者 副作用;必须在 onDispose 清理
rememberUpdatedState(value) Effect 不重启但要拿最新值 Effect 辅助;常用于最新回调
derivedStateOf 从已有状态计算新状态 派生状态;适合减少不必要重组或重复计算

十、总结

本篇你学到了 Compose 中副作用处理,以及相关状态工具的核心用法:

  1. 副作用概念:Composable 应该主要负责描述 UI,启动协程、注册监听、弹 Snackbar 这类事情属于副作用
  2. LaunchedEffect:适合在进入组合后或 key 变化后执行挂起任务
  3. rememberCoroutineScope:适合在点击事件中手动启动协程,比如弹 Snackbar
  4. DisposableEffect :适合注册需要清理的监听,并在 onDispose 中释放资源
  5. rememberUpdatedState:不是副作用本身,而是适合在不重启 Effect 的情况下拿到最新参数或回调
  6. derivedStateOf:不是副作用,而是适合从已有状态计算派生状态,避免额外保存一份容易不同步的数据
  7. 选择原则 :自动触发用 LaunchedEffect,用户事件用 rememberCoroutineScope,需要清理用 DisposableEffect

核心原则:不要把副作用直接塞进 Composable 函数体里,要把它放进合适的副作用 API 中,让 Compose 帮你处理启动、取消和清理

下一篇我们将学习 Compose 中的主题与 Material 3------如何统一管理颜色、字体、圆角和深色模式,让 App 从"能用"变成"像一个完整产品"。


如果你在学习过程中有任何疑问,欢迎在评论区留言,我会尽可能回复。

系列文章:

相关推荐
流氓也是种气质 _Cookie2 小时前
如何创建MCP
android·adb
Kapaseker2 小时前
扶我起来,Compose Styles API 进阶
android·kotlin
亘元有量-流量变现2 小时前
内外联动精细化运营:解锁安卓APP推广降本增效新路径
android·用户运营·aso优化·亘元有量·方糖试玩
nVisual3 小时前
nVisual:AI驱动的智能排障,平均响应时间缩短40%
android·人工智能·缓存
阿pin3 小时前
Android随笔-神经系统Handler
android·java·开发语言·handler
机智的张尼玛16 小时前
我基于 Kotlin Multiplatform 实现了一套跨平台的弱网离线同步引擎
android·开源·kotlin
安卓修改大师19 小时前
安卓修改大师反编译引擎Apktool深度解析:版本选择、参数说明与实战技巧
android
error:(20 小时前
【系统与实战双精通】VS Code 调试 ROS2 Python 节点与 Launch 系统指南
android·java·python
黄林晴21 小时前
Kuikly 是什么?和KMP有什么关系?
android·前端