Jetpack Compose 副作用完全解析(初学者必看,超详细)

先提前划重点:Compose的核心思想是"状态驱动UI",而副作用是"脱离UI渲染、影响外部环境的操作",Compose提供的副作用API,本质上是让这些操作"跟随Composable的生命周期",避免内存泄漏、重复执行等问题。

一、先搞懂:什么是Compose的副作用?

在讲具体API之前,我们必须先明确"副作用"的定义------不是什么高深的概念,用一句话就能说清楚:

副作用(Side Effect):在Composable函数中,除了"根据状态渲染UI"之外,所有影响外部环境(或被外部环境影响)的操作,都是副作用。

举几个初学者最容易接触到的副作用场景,一看就懂:

  • 网络请求(比如调用接口获取数据,影响了服务器,也被服务器返回的数据影响);
  • 日志打印(输出内容到控制台,影响了外部的日志系统);
  • 操作数据库(增删改查,影响了本地存储);
  • 注册/注销监听器(比如监听网络状态、传感器,影响了系统服务);
  • 启动协程、延迟操作(比如delay(2000),影响了线程调度);
  • 修改全局变量(比如修改一个单例中的属性,影响了函数外部的状态)。

那为什么不能直接在@Composable函数里写这些操作?比如下面这段代码,初学者很容易这么写,但其实是错误的:

kotlin 复制代码
@Composable
fun BadExample() {
    // 错误:直接在Composable中执行副作用(网络请求)
    val data = api.fetchData() // 网络请求,属于副作用
    Text(text = data.content)
}

原因很简单:Composable函数的执行是"不可预测"的------它可能会被多次重组(Recompose)、被取消、在后台线程执行,甚至重复执行。如果直接在里面写副作用,会导致:

  1. 重复执行:比如重组一次,就发起一次网络请求,造成资源浪费;
  2. 内存泄漏:比如注册了监听器,但没有注销,Composable销毁后监听器还在,导致内存泄漏;
  3. 状态混乱:比如协程还在执行,Composable已经重组,导致UI状态和协程执行结果不一致。

所以,Compose官方提供了一系列"副作用API",目的就是:让副作用操作"跟随Composable的生命周期",确保副作用只在合适的时机执行、在合适的时机清理,避免上述问题

这里先补充一个基础知识点(很重要):Composable的生命周期只有3个阶段,副作用API都是围绕这3个阶段设计的:

  1. 进入组合(Enter Composition):Composable首次被调用,加入到UI树中;
  2. 重组(Recompose):Composable依赖的状态发生变化,重新执行函数、更新UI(可能执行0次或多次);
  3. 退出组合(Leave Composition):Composable不再被使用(比如页面跳转、if判断为false),从UI树中移除。

所有副作用API,本质上都是"绑定"到这3个阶段,控制副作用的执行和清理时机。接下来,我们逐个解析最常用的5个副作用API,每个都讲透"是什么、怎么用、适用场景、避坑点"。

二、核心副作用API详解(逐个拆解,含完整示例)

我们按照"初学者使用频率"排序,从最常用的LaunchedEffect开始,逐个讲解,每个API都配套可直接复制运行的代码示例,以及详细的注释说明。

1. LaunchedEffect:最常用的"协程副作用"(必学)

1.1 是什么?

LaunchedEffect是Compose中最常用的副作用API,核心作用是:在Composable的作用域内,启动一个协程,并且让协程的生命周期和Composable完全绑定

简单说:当Composable进入组合时,LaunchedEffect会启动内部的协程;当Composable退出组合时,协程会被自动取消;当LaunchedEffect的"key"发生变化时,旧协程会被取消,新协程会重新启动。

它的核心优势是:无需手动管理协程的生命周期,避免协程泄漏(比如Composable销毁了,协程还在执行)。

1.2 基本用法(语法)

scss 复制代码
LaunchedEffect(keys = [key1, key2, ...]) {
    // 这里可以写挂起函数(suspend),比如网络请求、delay、Flow收集等
    // 协程会在Composable进入组合时启动,退出时自动取消
}

关键参数说明(初学者重点关注):

  • keys(可变参数):"触发重启"的关键。当keys中的任意一个值发生变化时,LaunchedEffect会取消当前正在运行的协程,重新启动一个新的协程;
  • 协程块:里面可以执行任何挂起函数(suspend),这是LaunchedEffect和其他副作用API的核心区别(专门用于协程相关的副作用)。

1.3 3个常用场景(含完整示例)

场景1:页面首次进入时,发起一次网络请求(最常用场景)

kotlin 复制代码
@Composable
fun UserProfileScreen(userId: String, viewModel: UserViewModel) {
    // 状态:用于存储请求到的用户数据
    val userState by viewModel.userData.collectAsState(initial = null)
    val loadingState by viewModel.loading.collectAsState(initial = false)
    val errorState by viewModel.error.collectAsState(initial = null)

    // 副作用:页面进入时,根据userId请求用户数据
    // keys = [userId]:当userId变化时,取消旧请求,发起新请求
    LaunchedEffect(key1 = userId) {
        // 启动协程,执行挂起函数(viewModel.fetchUser是挂起函数)
        viewModel.fetchUser(userId)
    }

    // 根据状态渲染UI(无副作用,纯渲染)
    when {
        loadingState -> CircularProgressIndicator() // 加载中
        errorState != null -> Text(text = "请求失败:${errorState}") // 错误提示
        userState != null -> {
            // 渲染用户信息
            Column(
                modifier = Modifier.padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                Text(text = "用户名:${userState?.name}", fontSize = 18.sp)
                Text(text = "手机号:${userState?.phone}")
                Text(text = "邮箱:${userState?.email}")
            }
        }
    }
}

// ViewModel中的挂起函数(示例)
class UserViewModel : ViewModel() {
    private val _userData = MutableStateFlow<User?>(null)
    val userData: StateFlow<User?> = _userData.asStateFlow()

    private val _loading = MutableStateFlow(false)
    val loading: StateFlow<Boolean> = _loading.asStateFlow()

    private val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error.asStateFlow()

    // 挂起函数:发起网络请求
    suspend fun fetchUser(userId: String) {
        _loading.value = true
        _error.value = null
        try {
            // 模拟网络请求(delay是挂起函数)
            delay(1500)
            val user = apiService.getUserById(userId) // 假设apiService是网络请求工具
            _userData.value = user
        } catch (e: Exception) {
            _error.value = e.message
        } finally {
            _loading.value = false
        }
    }
}

示例说明:

  • 当UserProfileScreen首次进入组合时,LaunchedEffect会启动协程,调用viewModel.fetchUser(userId)发起网络请求;
  • 如果userId发生变化(比如从"123"变成"456"),LaunchedEffect会取消之前的协程(如果请求还在进行),重新启动协程,请求新的用户数据;
  • 当页面跳转(UserProfileScreen退出组合),协程会被自动取消,避免请求完成后更新已销毁的UI,也避免内存泄漏。

场景2:一次性副作用(只执行一次,不随重组重启)

如果我们希望副作用只在Composable首次进入组合时执行一次,后续重组不重启,只需要将keys设为Unit(Unit是一个常量,永远不会变化)。

scss 复制代码
@Composable
fun SplashScreen(onNavigateToHome: () -> Unit) {
    // 副作用:只执行一次,延迟2秒后跳转到首页
    LaunchedEffect(key1 = Unit) {
        delay(2000) // 模拟启动页加载(挂起函数)
        onNavigateToHome() // 跳转首页
    }

    // 渲染启动页UI
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(text = "欢迎使用", fontSize = 24.sp, fontWeight = FontWeight.Bold)
    }
}

示例说明:keys = Unit,意味着LaunchedEffect只会在SplashScreen首次进入组合时启动一次协程,后续无论SplashScreen是否重组(比如父组件重组),协程都不会重新启动,完美实现"一次性启动页延迟跳转"。

场景3:监听Flow并更新UI(协程收集Flow)

在Compose中,收集Flow也是常见的副作用,通常用LaunchedEffect来启动协程收集,避免手动管理协程。

scss 复制代码
@Composable
fun MessageScreen() {
    // 状态:存储消息列表
    val messages = remember { mutableStateListOf<Message>() }
    // 模拟一个Flow(比如从数据库或WebSocket获取消息)
    val messageFlow = remember { flow {
        repeat(5) {
            delay(1000)
            emit(Message(content = "消息${it+1}", time = System.currentTimeMillis()))
        }
    } }

    // 副作用:收集Flow,将消息添加到列表中
    LaunchedEffect(key1 = messageFlow) {
        messageFlow.collect { message ->
            messages.add(message)
        }
    }

    // 渲染消息列表
    LazyColumn(modifier = Modifier.fillMaxSize()) {
        items(messages) { message ->
            Text(
                text = "${message.content}(${message.time})",
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

// 消息数据类
data class Message(val content: String, val time: Long)

示例说明:LaunchedEffect启动协程,收集messageFlow,每收到一条消息就添加到messages列表中,UI会自动重组更新;当MessageScreen退出组合时,协程会被取消,Flow收集也会停止,避免内存泄漏。

1.4 初学者避坑点

  • 坑1:在LaunchedEffect中修改非状态变量 → 导致UI不更新。比如在协程中直接修改普通变量,而不是MutableState或StateFlow,UI不会感知到变化;
  • 坑2:keys参数传错 → 比如不传keys(默认是emptyArray),会导致每次重组都重启协程;或者传了一个频繁变化的参数(比如Int类型的count),导致协程频繁取消和重启;
  • 坑3:在LaunchedEffect中执行非挂起函数 → 虽然可以,但没必要。LaunchedEffect的核心价值是管理协程和挂起函数,非挂起函数(比如普通日志打印)可以用其他更合适的副作用API。

2. DisposableEffect:需要"手动清理"的副作用(必学)

2.1 是什么?

DisposableEffect和LaunchedEffect类似,也是用于执行副作用,但它的核心场景是:副作用操作需要"手动清理" ------比如注册监听器、绑定系统服务、订阅广播等,这些操作如果不手动清理,会导致内存泄漏。

DisposableEffect的特点:

  • 进入组合时,执行副作用操作;
  • keys变化时,先执行"清理操作",再执行新的副作用操作;
  • 退出组合时,必须执行"清理操作"(编译器强制要求,不写会报错)。

注意:DisposableEffect不能执行挂起函数(这是和LaunchedEffect的核心区别),如果需要执行挂起函数,需要配合rememberCoroutineScope使用。

2.2 基本用法(语法)

scss 复制代码
DisposableEffect(keys = [key1, key2, ...]) {
    // 1. 执行副作用操作(比如注册监听器、绑定服务)
    val listener = MyListener()
    systemService.registerListener(listener)

    // 2. 必须返回一个清理函数(onDispose),编译器强制要求
    onDispose {
        // 清理操作:比如注销监听器、解绑服务
        systemService.unregisterListener(listener)
    }
}

关键说明:

  • keys参数:和LaunchedEffect一样,keys变化时,会先执行onDispose清理,再重新执行副作用;
  • onDispose函数:必须返回,这是DisposableEffect的核心,用于清理副作用产生的资源,避免内存泄漏;
  • 不能写挂起函数:如果需要在DisposableEffect中执行挂起操作,需要先通过rememberCoroutineScope获取协程作用域,再在作用域中启动协程。

2.3 2个常用场景(含完整示例)

场景1:注册/注销位置监听器(最典型场景)

kotlin 复制代码
@Composable
fun LocationTrackingScreen(userId: String) {
    val context = LocalContext.current
    // 状态:存储当前位置
    val currentLocation = remember { mutableStateOf<Location?>(null) }

    // 获取系统位置服务
    val locationManager = remember {
        context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    }

    // 副作用:注册位置监听器,退出时注销
    DisposableEffect(key1 = userId) {
        // 1. 创建监听器
        val locationListener = object : LocationListener {
            override fun onLocationChanged(location: Location) {
                // 位置更新,修改状态(触发UI重组)
                currentLocation.value = location
                // 可选:将位置上传到服务器(非挂起操作)
                uploadLocation(userId, location)
            }

            override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
            override fun onProviderEnabled(provider: String) {}
            override fun onProviderDisabled(provider: String) {}
        }

        // 2. 注册监听器(副作用操作)
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER,
                5000L, // 5秒更新一次
                10f, // 移动10米更新一次
                locationListener
            )
            Log.d("Location", "为用户$userId 注册位置监听器")
        }

        // 3. 清理操作:注销监听器(必须写)
        onDispose {
            locationManager.removeUpdates(locationListener)
            Log.d("Location", "为用户$userId 注销位置监听器")
        }
    }

    // 渲染UI
    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "用户$userId 的位置跟踪", fontSize = 18.sp, fontWeight = FontWeight.Bold)
        if (currentLocation.value != null) {
            Text(text = "当前经度:${currentLocation.value?.longitude}")
            Text(text = "当前纬度:${currentLocation.value?.latitude}")
        } else {
            Text(text = "正在获取位置...")
        }
    }
}

// 模拟位置上传(非挂起函数)
fun uploadLocation(userId: String, location: Location) {
    // 这里可以写网络请求(非挂起,比如用OkHttp同步请求,或另起协程)
    Log.d("Location", "上传用户$userId 位置:${location.latitude}, ${location.longitude}")
}

示例说明:

  • 当LocationTrackingScreen进入组合时,DisposableEffect会注册位置监听器,开始获取位置;
  • 当userId变化时,会先执行onDispose注销之前的监听器,再为新的userId注册新的监听器;
  • 当页面退出组合时,onDispose会自动执行,注销监听器,避免内存泄漏(如果不注销,locationManager会一直持有监听器引用,导致Composable无法被回收)。

场景2:监听Activity生命周期(结合LocalLifecycleOwner)

在Compose中,我们可以通过DisposableEffect监听传统Activity的生命周期(比如onStart、onStop),需要配合LocalLifecycleOwner使用。

kotlin 复制代码
@Composable
fun LifecycleListenerScreen() {
    // 获取当前的LifecycleOwner(通常是宿主Activity/Fragment)
    val lifecycleOwner = LocalLifecycleOwner.current

    // 副作用:监听生命周期,退出时移除监听器
    DisposableEffect(key1 = lifecycleOwner) {
        // 创建生命周期监听器
        val lifecycleObserver = object : LifecycleEventObserver {
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                // 根据生命周期事件做操作
                when (event) {
                    Lifecycle.Event.ON_START -> Log.d("Lifecycle", "Activity 进入前台")
                    Lifecycle.Event.ON_STOP -> Log.d("Lifecycle", "Activity 进入后台")
                    Lifecycle.Event.ON_DESTROY -> Log.d("Lifecycle", "Activity 销毁")
                    else -> {}
                }
            }
        }

        // 注册监听器
        lifecycleOwner.lifecycle.addObserver(lifecycleObserver)

        // 清理操作:移除监听器
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
            Log.d("Lifecycle", "移除生命周期监听器")
        }
    }

    // 渲染UI
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        Text(text = "监听Activity生命周期", fontSize = 20.sp)
    }
}

示例说明:

  • LocalLifecycleOwner.current获取当前Composable的宿主生命周期所有者(比如Activity);
  • DisposableEffect注册生命周期监听器,监听Activity的ON_START、ON_STOP等事件;
  • 退出组合时,onDispose移除监听器,避免内存泄漏。

2.4 初学者避坑点

  • 坑1:忘记写onDispose → 编译器会直接报错,这是DisposableEffect的强制要求,即使没有清理操作,也要写空的onDispose {};
  • 坑2:在DisposableEffect中执行挂起函数 → 会报错,因为DisposableEffect的代码块是普通函数,不是挂起函数;如果需要执行挂起操作,需要配合rememberCoroutineScope;
  • 坑3:keys参数传错 → 比如传了一个频繁变化的参数,导致频繁注册/注销,影响性能(比如传count,每次count变化都要重新注册监听器)。

3. SideEffect:每次重组后执行的副作用(简单但常用)

3.1 是什么?

SideEffect是最"简单"的副作用API,核心作用是:在每次Composable重组成功后,执行一次副作用操作

它的特点:

  • 没有keys参数,每次重组成功(UI更新完成)后都会执行;
  • 不需要手动清理(适合不需要清理的简单副作用);
  • 不能执行挂起函数;
  • 执行时机:重组完成后,UI已经更新到屏幕上。

适用场景:简单的日志打印、埋点统计、将Compose状态同步到非Compose代码(比如传统View、单例类)。

3.2 基本用法(语法)

arduino 复制代码
SideEffect {
    // 执行副作用操作(非挂起、无需清理)
    // 每次重组成功后都会执行
    Log.d("SideEffect", "Composable重组完成")
    analytics.trackScreenView("HomeScreen") // 埋点统计
}

3.3 常用场景(含完整示例)

场景:埋点统计(每次重组都更新当前页面的埋点信息)

kotlin 复制代码
// 模拟一个非Compose的埋点工具类(传统代码)
object AnalyticsTracker {
    // 记录当前页面的步骤(比如注册流程的步骤)
    fun trackRegistrationStep(step: Int, isCompleted: Boolean) {
        Log.d("Analytics", "注册步骤:$step,是否完成:$isCompleted")
        // 实际开发中,这里会调用埋点SDK(比如友盟、百度统计)
    }
}

@Composable
fun RegistrationScreen() {
    // 状态:当前注册步骤(1-3)
    var currentStep by remember { mutableStateOf(1) }
    // 状态:当前步骤是否完成
    var isStepCompleted by remember { mutableStateOf(false) }

    // 副作用:每次重组成功后,上报当前步骤信息(埋点)
    SideEffect {
        AnalyticsTracker.trackRegistrationStep(currentStep, isStepCompleted)
    }

    // 渲染注册步骤UI
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "注册步骤 $currentStep", fontSize = 20.sp, fontWeight = FontWeight.Bold)

        Button(onClick = {
            // 模拟完成当前步骤
            isStepCompleted = true
        }) {
            Text(text = "完成当前步骤")
        }

        Button(
            onClick = {
                if (currentStep < 3) {
                    currentStep++
                    isStepCompleted = false
                }
            },
            enabled = isStepCompleted
        ) {
            Text(text = "进入下一步")
        }
    }
}

示例说明:

  • 当currentStep或isStepCompleted变化时,Composable会重组;
  • 重组成功后,SideEffect会自动执行,调用AnalyticsTracker.trackRegistrationStep上报当前步骤信息;
  • 不需要清理操作,因为埋点操作是"一次性"的,不会产生需要回收的资源。

3.4 初学者避坑点

  • 坑1:在SideEffect中执行需要清理的操作 → 比如注册监听器,会导致内存泄漏(SideEffect没有onDispose);
  • 坑2:在SideEffect中执行耗时操作 → 会阻塞UI线程,因为SideEffect在主线程执行,且每次重组都会执行;
  • 坑3:误以为SideEffect只执行一次 → 其实每次重组成功都会执行,比如点击按钮修改状态,导致重组,SideEffect就会再执行一次。

4. rememberCoroutineScope:手动控制的协程作用域(补充API)

4.1 是什么?

rememberCoroutineScope不是"副作用API",但它和副作用密切相关,核心作用是:获取一个和Composable生命周期绑定的协程作用域(CoroutineScope) ,用于手动启动协程(比如用户点击事件触发的协程)。

它和LaunchedEffect的区别(重点,初学者容易混淆):

特性 LaunchedEffect rememberCoroutineScope
启动时机 Composable进入组合时,自动启动协程 手动调用scope.launch,按需启动协程(比如点击事件)
协程管理 自动管理生命周期(进入启动、退出取消、key变化重启) 手动管理协程(需手动cancel,但Composable退出时会自动取消作用域)
适用场景 自动执行的协程(页面进入请求数据、Flow收集) 用户交互触发的协程(点击按钮发起请求、延迟提示)

4.2 基本用法(语法)

scss 复制代码
// 获取协程作用域(和Composable生命周期绑定)
val scope = rememberCoroutineScope()

// 手动启动协程(比如点击事件中)
Button(onClick = {
    scope.launch {
        // 执行挂起函数
        delay(1000)
        showToast(context, "点击成功")
    }
}) {
    Text(text = "点击我")
}

4.3 常用场景(含完整示例)

场景:用户点击按钮,延迟1秒显示Toast(手动触发协程)

kotlin 复制代码
@Composable
fun DelayedToastButton() {
    val context = LocalContext.current
    // 获取协程作用域(和当前Composable生命周期绑定)
    val scope = rememberCoroutineScope()

    Button(
        onClick = {
            // 手动启动协程(用户点击时触发)
            scope.launch {
                // 执行挂起函数(延迟1秒)
                delay(1000)
                // 显示Toast(非挂起操作)
                Toast.makeText(context, "点击成功!", Toast.LENGTH_SHORT).show()
            }
        },
        modifier = Modifier.padding(16.dp)
    ) {
        Text(text = "点击我,1秒后显示Toast")
    }
}

示例说明:

  • rememberCoroutineScope获取的scope,会和当前Composable的生命周期绑定;
  • 用户点击按钮时,手动调用scope.launch启动协程,延迟1秒后显示Toast;
  • 如果Composable在协程执行过程中退出组合(比如页面跳转),scope会被自动取消,协程也会停止,避免Toast在页面销毁后还显示。

4.4 初学者避坑点

  • 坑1:用GlobalScope替代rememberCoroutineScope → GlobalScope的协程不与任何Composable绑定,Composable退出后协程还会继续执行,容易导致内存泄漏;
  • 坑2:忘记手动取消协程 → 虽然scope会在Composable退出时自动取消,但如果需要在某个条件下手动取消(比如用户再次点击),需要保存协程的Job,调用job.cancel();
  • 坑3:在rememberCoroutineScope中启动的协程,没有和keys绑定 → 如果需要根据状态变化取消协程,需要手动管理(比如用LaunchedEffect更合适)。

5. rememberUpdatedState:获取最新状态的"保鲜剂"(进阶API)

5.1 是什么?

rememberUpdatedState是一个"辅助API",核心作用是:在副作用中,获取状态的"最新值",即使副作用没有被重启

举个例子:如果我们用LaunchedEffect启动一个长期运行的协程(比如循环执行的动画),协程中需要用到某个状态,但我们不希望这个状态变化时,协程被取消重启(因为重启会中断动画),这时候就可以用rememberUpdatedState来获取状态的最新值。

简单说:rememberUpdatedState可以"保鲜"状态的最新值,让副作用在不重启的情况下,使用最新的状态。

5.2 基本用法(语法)

scss 复制代码
// 原始状态(可能会变化)
var state by remember { mutableStateOf(initialValue) }

// 用rememberUpdatedState包裹,获取最新值
val updatedState = rememberUpdatedState(state)

// 在副作用中使用updatedState.value(始终是最新值)
LaunchedEffect(Unit) {
    while (true) {
        delay(1000)
        Log.d("UpdatedState", "最新状态:${updatedState.value}")
    }
}

5.3 常用场景(含完整示例)

场景:长期运行的动画,根据状态变化调整动画速度(不重启协程)

ini 复制代码
@Composable
fun PulseAnimation() {
    // 状态:动画间隔(用户可以调整,比如3000ms → 1000ms)
    var pulseInterval by remember { mutableStateOf(3000L) }
    // 状态:动画透明度(0f → 1f → 0f 循环)
    val alpha = remember { Animatable(1f) }

    // 用rememberUpdatedState包裹pulseInterval,获取最新值
    val updatedPulseInterval = rememberUpdatedState(pulseInterval)

    // 副作用:启动长期运行的协程(循环执行动画)
    // keys = Unit:只启动一次,不随pulseInterval变化而重启
    LaunchedEffect(Unit) {
        while (isActive) { // isActive:协程是否活跃
            // 使用updatedPulseInterval.value(始终是最新的间隔时间)
            delay(updatedPulseInterval.value)
            // 执行动画(挂起函数)
            alpha.animateTo(0f, animationSpec = tween(500))
            alpha.animateTo(1f, animationSpec = tween(500))
        }
    }

    // 渲染UI:动画文本 + 调整间隔的按钮
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "脉冲动画",
            fontSize = 24.sp,
            modifier = Modifier.graphicsLayer { this.alpha = alpha.value }
        )

        Row(
            modifier = Modifier.padding(top = 32.dp),
            horizontalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Button(onClick = { pulseInterval = 3000L }) {
                Text(text = "慢节奏(3秒)")
            }
            Button(onClick = { pulseInterval = 1000L }) {
                Text(text = "快节奏(1秒)")
            }
        }
    }
}

示例说明:

  • LaunchedEffect的keys = Unit,意味着协程只启动一次,不会随pulseInterval变化而重启;
  • 用rememberUpdatedState包裹pulseInterval,在协程中使用updatedPulseInterval.value,即使pulseInterval变化,协程也不会重启,但能获取到最新的间隔时间;
  • 这样既保证了动画的连续性(不被重启中断),又能根据用户操作调整动画速度,完美解决"既要最新状态,又不重启副作用"的需求。

5.4 初学者避坑点

  • 坑1:滥用rememberUpdatedState → 只有当"副作用不能重启,但需要获取最新状态"时才用,否则用普通状态即可;
  • 坑2:忘记用.value获取值 → rememberUpdatedState返回的是一个State对象,必须通过.value才能获取到最新值;
  • 坑3:在rememberUpdatedState中使用不可变状态 → 没有意义,因为不可变状态不会变化,不需要"保鲜"。

三、总结:5个副作用API对比(初学者快速查阅)

为了方便大家记忆和快速选择合适的API,我整理了一张对比表,涵盖核心特点、适用场景和关键注意点:

API名称 核心特点 适用场景 关键注意点
LaunchedEffect 自动启动协程,绑定Composable生命周期,支持keys重启 网络请求、Flow收集、动画、一次性延迟操作 可执行挂起函数,keys决定是否重启
DisposableEffect 需手动清理,支持keys重启,编译器强制要求onDispose 注册/注销监听器、绑定/解绑系统服务 不能执行挂起函数,必须写onDispose
SideEffect 每次重组成功后执行,无需清理,无keys 埋点统计、日志打印、同步状态到非Compose代码 不能执行挂起函数,每次重组都执行
rememberCoroutineScope 获取协程作用域,手动启动协程,绑定生命周期 用户交互触发的协程(点击按钮、手势) 不要用GlobalScope替代,需手动管理协程
rememberUpdatedState 保鲜状态最新值,不重启副作用 长期运行的副作用,需要获取最新状态但不重启 必须通过.value获取值,不滥用

四、初学者实战建议(必看)

看完上面的解析,可能有初学者会觉得"知识点太多,记不住",这里给大家3个实战建议,帮助大家快速上手:

  1. 先掌握核心3个API:LaunchedEffect、DisposableEffect、SideEffect,这3个覆盖了80%的开发场景,rememberCoroutineScope和rememberUpdatedState可以后续再深入;
  2. 写代码时,先判断"是否是副作用":如果是网络请求、协程、监听器、日志,就用对应的副作用API,不要直接写在Composable函数体中;
  3. 多练多踩坑:把上面的示例代码复制到项目中,运行起来,修改参数(比如keys、状态值),观察副作用的执行和清理时机,比单纯看理论更有效。

最后提醒一句:Compose的副作用API,核心都是"让副作用跟随Composable的生命周期",只要记住这一点,就能避免大部分坑。比如:需要协程 → LaunchedEffect;需要清理 → DisposableEffect;简单打印 → SideEffect;手动触发 → rememberCoroutineScope。

如果大家在使用过程中遇到具体问题(比如协程泄漏、副作用不执行),可以在评论区留言,我会一一解答。

好了,今天的Jetpack Compose副作用解析就到这里,希望这篇超详细的博客能帮助初学者快速掌握副作用的使用,少走弯路。后续我还会分享更多Compose的实战技巧,记得关注哦!

相关推荐
零陵上将军_xdr2 小时前
MySQL 事务写入流程详解
android·数据库·mysql
2501_915921434 小时前
苹果iOS应用开发上架与推广完整教程
android·ios·小程序·https·uni-app·iphone·webview
jian110585 小时前
Android studio gradle和插件的版本设置
android·ide·android studio
idolao5 小时前
Android Studio 2022安装与汉化教程 Windows版:解压+管理员运行+自定义路径+SDK配置+中文插件指南
android·windows·android studio
2501_915106325 小时前
HTTP和HTTPS协议工作原理及安全性全面解析
android·ios·小程序·https·uni-app·iphone·webview
古阙月5 小时前
嘉立创PCB设计初级总结
android·pcb工艺
Dream of maid5 小时前
Mysql(7)子查询
android·数据库·mysql
恋猫de小郭6 小时前
compose_skill 和 android skills,对 Android 项目提升巨大的专家 AI Skills
android·前端·flutter