Jetpack Compose -> 重组作用域和remember()

前言


上一章我们讲解了 MutableState 和 mutableStateOf() 本章我们讲解下 remember 这个关键方法;

ReCompose Scope(重组作用域)


我们先来看一段代码

infoflow 2024-02-20 15-12-01.png

当我们将

var name by mutableStateOf("老A")

lifecycleScope.launch{}

这两行代码放到 setContent 中的时候,它提示红色了,显然这样写,Compose 是不允许的(这里其实是可以运行的,但是运行之后没有任何的效果,也就是 name 的值在3s后并没有变成 “Mars”),那么这是为什么呢?

这里面,我们写的协程是没有任何问题的,它也正常的执行了,但是真正影响我们效果的是 mutableStateOf 提示的那行代码;

我们前面有说到,Compose 会在使用到这些变量的值改变的时候得到通知,从而进行重组(recompose) ,例如 Text(name) 它会在 name 变化的时候得到通知,进行重组,但是我们来具体想一下,它怎么被执行的呢?比如说:我们现在需要写一段代码,需要在运行的时候,从界面上指定一段代码去重复执行,例如重复执行 Text(name) 这行代码,那么应该怎么实现呢?基本思路就是拿到这行代码,然后执行它,但是这好像不太能实现,在 java 也好 kotlin 中也罢,几乎是不太能实现的;

但是在 Compose 中,它替我们进行了实现,Compose 的编译器插件会修改逻辑,它会把这些可能会被重新调用的代码块给包了起来,然后在这个被包起来的代码块执行完成之后,它会把这个代码块保存下来,并标记到当前它被执行的这个位置,去做个标记,当重新执行的条件达成的时候,比如说:当 name 的值发生改变的时候,Text(name) 所在的代码块就会发生 Recompose 具体来说这个所在代码块它会重新执行,并且执行的时候它所依赖的变量值就是最新的值,从而就能基于最新值组合出最新的界面;如果组合的结果和上一次的结果不同,那么布局和绘制流程也会重新再走一遍,从而有更新界面的显示,如果组合的结果和上次的结果相同,说明界面的实质内容没有改变,则不会重新绘制;

Compose 并不是包裹所有的代码,而是包裹需要包裹的代码,什么是需要包裹的代码?就是可能会发生变化的代码,例如 Text(name) 这个 name 是一个变量,这个 name 改变了,这个 Text 就需要重新执行,那么这个就是需要被包裹的代码;

也就是说 ReCompose 并不是全局的,而是哪里需要做才做,这种被包裹起来在 ReCompose 的时候一起执行的代码范围就被 Compose 称作 ReCompose Scope(重组作用域)

所以代码执行的问题就在

var name by mutableStateOf("老A")

这行代码上,它重新执行的时候,并不是 Text(name) 这一行了,而是一个范围了,这个范围内的代码都会被重新执行了,自然也就包含了 name 的初始化过程,那么这就意味着当 Text(name) 执行的时候,它所用到的值已经不是刚才被协程修改过的值了(“Mars”),而是一个被重新初始化的值了,也就是 “老A” 这个字符串,本质上不是赋值失败,而是赋值之后触发了 ReCompose,并且 ReCompose 的范围波及到了更多的代码,导致重新创建了一个新的 name 对象,并且 Text(name) 重新调用的时候,采用的是这个新的值;而那个旧的 name 确实被改成了 “Mars” 但是 Text 用的不是这个旧的 name 了,而是一个新的 name;

那么怎么解决呢?把 Text(name) 包裹起来就可以了

var name by mutableStateOf("老A")
// 这样包裹一下
Button(onClick = { /*TODO*/ }) {
    Text(text = name)
}
lifecycleScope.launch {
    delay(3000)
    name = "Mars"
}

remember


但是,我们在业务开发的时候,肯定是不能这么写的(业务场景也不允许嘛),那么我们还有其他的方法来规避吗?我们来看下 IDE 有没有给提供解决方案,我们把鼠标放到警告的地方看下:

infoflow 2024-02-22 14-47-56.png

Creating a state object during composition without using remember

我们没有使用 remember 来创建一个 state object,也就是需要我们使用 remember 来包一下这个 state object

var name by remember { mutableStateOf("老A") } 

可以看到红线警告消失了;

remember 所做的事情就是:在它第一次执行的时候,它会执行 lambda 中的代码 {mutableStateOf(“老A”)} 同时呢,它也会保存这个结果,再次调用这个 remember 的时候,它就会直接返回这个保存的结果,remember 在这里起到了一个缓存的作用;

我们把 Button 包裹去掉看下效果:

setContent {
    Android_VMTheme {
        Surface {
            var name by remember { mutableStateOf("老A") } 
            Text(text = name)
            lifecycleScope.launch {
                delay(3000)
                name = "Mars"
            }
            // Ui()
        }
    }
}

可以看到,3s 后变成了『Mars』,也就是那个缓存的值被改成了 Mars,二次初始化拿到的是老的值;

所以说这个 remember 在 Compose 中是很有用的,它可以防止由于 ReCompose 而导致的预期之外的某些变量的反复初始化,这些反复初始化的变量可能会带来意料之外的结果,但是加一个 remember 这些问题就可以解决了;

但是我们在实际业务开发的时候,怎么去判断哪些变量会被 ReCompose 呢?我们是无法判断的,那怕这段逻辑开发这很清楚,但是一旦被外部调用的时候,它就不可控了;

所以 Compose 针对所有的 mutableStateOf() 都加上 remember 包裹;

这里有一点要注意:我们用 remember 包裹是为了防止被 Compose 包裹的代码执行 ReCompose 从而导致变量的反复初始化带来的意料之外的结果;

如果变量的初始化没有被 Compose 包裹,也就是,我们把变量的初始化放到了外面

infoflow 2024-02-22 15-06-45.png

可以看到,remember 上进行了红色警告

@Composable invocations can only happen from the context of a @Composable function

也就是说我们不能使用 remember 关键字,所以说,无论 Text(name) 怎样 ReCompose 都不会导致 name 变量的重新初始化;

因为 remember 是一个 Composable 函数,它只能在另一个 Composable 函数中被调用

infoflow 2024-02-22 18-29-52.png

这个还会导致编译失败;

带参数的 remember

@Composable
fun Request(value: String) {
    val length = remember {
        value.length
    }
    Text(text = "长度为$length")
}

如果我们每次调用这个 Request 方法的时候,传递的 value 值都是一样的,那么这段代码执行起来就没有问题,一旦我们传入的值不一样的时候,那么这个段代码执行就有问题了;

例如:第一次 mars

第二次 mars

第三次 old a

这个时候,如果使用了 remember 那么 length 的长度就不对了,

怎么解决这个问题呢? remember 提供了带参数的方法,这个带参数的意思是:虽然我可以缓存,但是我要给缓存加上一个或者几个 key,如果 key 和上次一样,我就用缓存的,如果不一样,我就不使用缓存了;

@Composable
fun Request(value: String) {
    val length = remember(value) {
        value.length
    }
    Text(text = "长度为$length")
}

这个意思就是 value 会影响 lambda 中的计算结果,如果 value 没有变就不计算了,直接返回结果,变了就执行计算逻辑;

好了,今天的内容就到这里吧~~

下一章预告


无状态,状态提升,单向数据流

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力

<think>我们正在讨论Jetpack Compose中的副作用(Side Effects)。在声明式UI中,副作用是指那些发生在可组合函数之外的操作,例如修改共享状态、发起网络请求或访问外部资源等。由于Compose的可组合函数可能会在重组中多次执行,因此需要一种机制来确保副作用在适当的时机执行,且不会重复执行或导致资源泄漏。 根据引用[2][3][4],在Jetpack Compose中,管理副作用非常重要,因为它可以优化性能资源利用,避免不必要的UI重组计算。同时,一些必要的副作用(如网络请求、数据库操作等)需要在受控环境中执行。 Jetpack Compose提供了一系列副作用API,如`LaunchedEffect`、`DisposableEffect`、`SideEffect`等,用于在可组合函数中安全地执行副作用。 ### 1. LaunchedEffect `LaunchedEffect`用于在可组合函数中启动一个协程,并在其键(key)发生变化时重新启动协程。当退出组合时,协程会自动取消。这适用于执行需要协程作用域的操作,如网络请求。 示例(来自引用[5]): ```kotlin @Composable fun LaunchedEffectDemo() { var count by remember { mutableStateOf(0) } var result by remember { mutableStateOf(0) } LaunchedEffect(count) { // 当count变化时,重新启动协程 println("发生了变化") delay(1000) // 模拟网络请求 result++ // 更新状态(注意:这里直接修改状态,但在实际中可能需要线程安全考虑) } Column(...) { Text(text = "Count: $count result=$result") Button(onClick = { count++ }) { Text("加") } } } ``` ### 2. DisposableEffect `DisposableEffect`用于执行需要清理的副作用。它在键发生变化或退出组合时执行清理操作。例如,注册注销广播接收器。 示例: ```kotlin @Composable fun DisposableEffectExample() { val context = LocalContext.current DisposableEffect(Unit) { // 使用Unit表示只执行一次(在组合时) val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { // 处理广播 } } context.registerReceiver(broadcastReceiver, IntentFilter("ACTION")) // 清理函数 onDispose { context.unregisterReceiver(broadcastReceiver) } } } ``` ### 3. SideEffect `SideEffect`用于将Compose状态发布到非Compose管理的对象(例如,将状态同步到外部对象)。它在每次成功重组后执行,且没有键参数。 示例: ```kotlin @Composable fun SideEffectExample(analytics: Analytics, userId: String?) { SideEffect { // 将userId同步到分析工具 analytics.setUserId(userId) } } ``` ### 4. rememberCoroutineScope 当需要在可组合函数之外启动协程(例如在按钮点击事件中)时,可以使用`rememberCoroutineScope`来获取一个绑定到当前组合的协程作用域。当退出组合时,该作用域会取消所有子协程。 示例: ```kotlin @Composable fun RememberCoroutineScopeExample() { val scope = rememberCoroutineScope() var count by remember { mutableStateOf(0) } Button(onClick = { scope.launch { delay(1000) count++ } }) { Text("增加") } } ``` ### 5. produceState `produceState`用于将非Compose状态转换为Compose状态。它会启动一个协程,并返回一个`State`对象,该对象的值由协程更新。 示例: ```kotlin @Composable fun ProduceStateExample(url: String): State<Result<Data>> { return produceState<Result<Data>>(initialValue = Result.Loading, url) { // 在后台协程中获取数据 val data = fetchData(url) value = if (data == null) Result.Error else Result.Success(data) } } ``` ### 使用副作用的注意事项 - **避免在可组合函数中直接执行副作用**:例如,不要在可组合函数中直接发起网络请求或修改共享状态,因为重组可能随时发生。 - **使用合适的副作用API**:根据副作用的类型(是否需要协程、是否需要清理等)选择合适的API。 - **键(Key)的使用**:在`LaunchedEffect`、`DisposableEffect`等中,键的变化会重新启动副作用。如果键是常量(如`Unit`),则副作用只会在进入组合时执行一次。 - **避免在副作用中修改触发该副作用的键**:这可能导致无限循环。 ### 总结 Jetpack Compose的副作用机制提供了一套在声明式UI中安全执行外部操作的方法。通过使用副作用API,开发者可以在不破坏声明式模型的前提下,处理必要的副作用,同时确保资源得到合理利用及时释放。 §§相关问题§§ 1. Jetpack Compose中哪些副作用API用于需要清理的操作? 2. 如何在按钮点击事件中安全地启动协程? 3. 使用`LaunchedEffect`时如何避免无限循环? 4. `SideEffect``LaunchedEffect`的主要区别是什么?</think>### Jetpack Compose 副作用详解及使用指南 在 Jetpack Compose 的声明式 UI 框架中,**副作用(Side Effect)** 指在可组合函数中执行的影响外部状态的操作(如网络请求、数据库访问、页面跳转等)。由于可组合函数可能被频繁重组(Recomposition),直接执行副作用可能导致资源泄漏或不可预期的行为。为此,Compose 提供了专门的副作用 API 来安全地管理这些操作[^2][^3]。 --- #### 一、为什么需要管理副作用? 1. **重组特性** `@Composable` 函数会因状态变化被多次调用。若直接执行副作用(如网络请求),可能导致重复操作或资源泄漏[^4]。 2. **声明式原则** Compose 要求 UI 描述是幂等的(相同输入产生相同输出),副作用会破坏这一原则[^3]。 3. **生命周期感知** 副作用需在组合的生命周期(进入/退出)中受控执行,避免后台任务在 UI 销毁后继续运行[^2]。 --- #### 二、核心副作用 API 及使用场景 ##### 1. `LaunchedEffect`:协程作用域内的副作用 - **适用场景**:网络请求、延时任务、动画等需协程支持的操作。 - **特性**: - 在组合时启动协程,退出组合时自动取消。 - 通过 `key` 参数控制重启(当 `key` 变化时重启协程)[^5]。 ```kotlin @Composable fun UserProfile(userId: String) { var userData by remember { mutableStateOf<User?>(null) } // 当 userId 变化时重启协程 LaunchedEffect(userId) { userData = fetchUserData(userId) // 模拟网络请求 } if (userData != null) { Text(text = userData.name) } else { CircularProgressIndicator() } } ``` ##### 2. `DisposableEffect`:需资源清理的副作用 - **适用场景**:监听器注册/注销、广播接收、资源释放。 - **特性**:通过 `onDispose` 回调确保资源清理[^3]。 ```kotlin @Composable fun LocationTracker() { val context = LocalContext.current var location by remember { mutableStateOf("") } DisposableEffect(Unit) { // Unit 表示只执行一次 val locationListener = LocationListener { loc -> location = "(${loc.lat}, ${loc.lon})" } registerLocationListener(locationListener) onDispose { unregisterLocationListener(locationListener) // 退出组合时清理 } } Text("位置: $location") } ``` ##### 3. `SideEffect`:与重组同步的轻量级副作用 - **适用场景**:将 Compose 状态同步到非 Compose 对象(如分析工具)。 - **特性**:每次成功重组后执行,无键值依赖[^4]。 ```kotlin @Composable fun AnalyticsTracker(screenName: String) { val analytics = remember { AnalyticsService() } SideEffect { analytics.trackScreenView(screenName) // 每次重组后同步状态 } } ``` ##### 4. `rememberCoroutineScope`:事件触发的副作用 - **适用场景**:用户交互(如按钮点击)触发的异步操作。 - **特性**:协程作用域与组合生命周期绑定[^2]。 ```kotlin @Composable fun Counter() { var count by remember { mutableStateOf(0) } val scope = rememberCoroutineScope() // 绑定到当前组合的作用域 Button(onClick = { scope.launch { delay(1000) // 模拟耗时操作 count++ } }) { Text("点击 (+1)") } } ``` --- #### 三、最佳实践 1. **避免在可组合函数中直接执行副作用** 错误示例:`@Composable fun Demo() { fetchData() }` ❌ 正确做法:使用 `LaunchedEffect` 包裹 ✅ 2. **合理设置副作用重启条件** - 无 `key`:副作用只在初始组合执行一次。 - 带 `key`:当 `key` 变化时重启(如示例中的 `userId`)[^5]。 3. **资源必须释放** 使用 `DisposableEffect` 的 `onDispose` 确保监听器、连接等被清理。 4. **避免副作用中的状态修改触发无限循环** 例如:在 `LaunchedEffect` 中更新其依赖的 `key` 会导致无限重启。 --- #### 四、常见问题解决方案 **问题 1**:网络请求重复触发 **解决**:确保 `LaunchedEffect` 的 `key` 稳定(如使用唯一 ID 而非对象引用)。 **问题 2**:退出界面后后台任务未取消 **解决**:使用 `LaunchedEffect` 或 `rememberCoroutineScope`,它们会在组合退出时自动取消协程。 **问题 3**:状态同步不及时 **解决**:对非 Compose 状态使用 `SideEffect`,确保重组后立即同步。 --- #### 总结 Jetpack Compose 的副作用 API(`LaunchedEffect`, `DisposableEffect`, `SideEffect`)为声明式 UI 中的外部操作提供了生命周期感知的安全管理机制。开发者应根据场景选择合适的 API,并遵循“资源清理”“避免无限循环”原则,以构建高性能、可维护的应用[^2][^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值