关于Jetpack Compose重绘(Recomposition)的一个坑

104 篇文章 29 订阅

在这里插入图片描述
最近尝鲜Jetpack Compose时,踩了一个坑,可能是很多人都容易忽略的问题,特此记录一下。
当前最新版本: 1.0.0-alpha11

Compose重绘


关于Compose的重绘(Recomposition),官方是这样介绍的:

Recomposition skips as much as possible
When portions of your UI are invalid, Compose does its best to recompose just the portions that need to be updated. This means it may skip to re-run a single Button’s composable without executing any of the composables above or below it in the UI tree.

下面是官方配套的例子:

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        // LazyColumnFor is the Compose version of a RecyclerView.
        // The lambda passed is similar to a RecyclerView.ViewHolder.
        LazyColumnFor(names) { name ->
            // When an item's [name] updates, the adapter for that item
            // will recompose. This will not recompose when [header] changes
            NamePickerItem(name, onNameClicked)
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

根据官方文档的介绍,当Composable函数签名中的参数不发生变化时,不应重绘,从而提高整体重绘性能。但经过测试这仅限于调用外部Composable函数的时候,对于内部直接调用的DSL,则无法通过参数判断是否参与重绘:

如下:

@Composable
fun ParentComponent(
	list: List<Data>
) {
	Log.d("compose", "render parent")
	ChildComponent(list) 
}

@Composable
fun ChildComponent(list :List<Data>) {
	Box{
		Log.d("compose", "render child")
	}
}

由于ChildComponent签名中依赖了list,当list变化引起重绘时的日志如下

D/compose: render parent
D/compose: render child

如果ChildComponent改为无参函数:

@Composable
fun ParentComponent(
	list: List<Data>
) {
	Log.d("compose", "render parent")
	ChildComponent() 
}

@Composable
fun ChildComponent() {
	Box{
		Log.d("compose", "render child")
	}
}
D/compose: render parent

由于对list不再依赖,当list变化引起重绘时child不再重绘

内联组件的重绘


当我们把child直接内联到parent中时:

@Composable
fun ParentComponent(
	list: List<Data>
) {
	Log.d("compose", "render parent")
	Box{
		Log.d("compose", "render child")
	}
}
D/compose: render parent
D/compose: render child

虽然Box{ ... }内部没有对list的依赖,但是仍然参与了重绘。

按照正常的思考,一段逻辑,无论是否抽成独立的函数,应该不影响其执行时的行为。但是对于Composable函数,这里却反常识的。

原因猜想


试着猜想一下原因:

我们知道Composable函数在编译期会生成很多逻辑代码,包括是否参与重绘的检查逻辑:仅当参数发生变化时重新执行Composable。

但是对于内联的情况,因为闭包可以随意访问外部成员,所以无法通过参数简单的判断,这会增加编译期的工作量,所以目前DSL的尾lambda中无论是否引用了变化的数据都会参与重绘。

为了验证一下猜想,我们把提出的函数加上inline试了一下:

@Composable
fun ParentComponent(
	list: List<Data>
) {
	Log.d("compose", "render parent")
	ChildComponent() 
}

@Composable
inline fun ChildComponent() {
	Box{
		Log.d("compose", "render child")
	}
}
D/compose: render parent
D/compose: render child

果不其然,添加inline后,即使没有参数依赖仍然参与重绘

最后


这个不知道算不算BUG,但肯定是需要开发者注意的地方,不要想当然的判断某些子组件或许不参与重绘,所以在实现里加载了一些私货。任何Composable函数都要以纯函数的标准去实现,无论是否多次重绘都不影响其行为。

当然也希望官方能在未来的版本中能够对其优化,无论是否是内联组件,行为能够保持一致,避免为开发者带来困惑。

Jetpack Compose 中,如果你想使用 `LazyColumn` 创建一个类似宫格式的网格列表,可以结合 `Grid` 或者 `Row` 控件以及 `RecyclerGridState`。下面是一个基本的示例,展示如何将项目分组到网格格子中: ```kotlin import androidx.compose.foundation.layout.Arrangement.RowMajor import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Grid import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable fun GridListWithColumns(groups: List<List<String>>) { LazyColumn { items(groups) { group -> Grid( columns = 3, spacing = 8.dp, contentPadding = PaddingValues(vertical = 8.dp), modifier = Modifier.fillMaxWidth(), arrangement = Arrangement.RowMajor ) { group.forEachIndexed { index, item -> if (index % 3 == 2) { Spacer(modifier = Modifier.size(0.dp)) } else { Button(onClick = { /* Your click action here */ }) { Text(text = item, style = MaterialTheme.typography.body2) } } } } } } } // 使用时传递数据 val data = listOf( listOf("Group 1 Item 1", "Group 1 Item 2", "Group 1 Item 3"), listOf("Group 2 Item 1", "Group 2 Item 2", "Group 2 Item 3"), // 更多数据... ) GridListWithColumns(data) ``` 这个例子中,我们创建了一个懒加载列,每行显示三个元素,当到达网格的最后一行时,会插入一个空隙。每个项目的点击动作可以根据实际需求来实现。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值