背景
在本篇文章中,我们将探讨在Jetpack Compose中提升应用性能的基础实践,重点关注那些应当采取与避免的操作。借助具体的代码示例,我们将揭示如何通过代码中的微小调整,实现应用程序效率和性能的显著增强。
Stable&Immutable
首先是使用Stable和Immutable注解来避免非要的重组。
在Jetpack Compose中,如果一个类中包含了可变变量(var)或可变集合(例如ArrayList),编译器会默认视该类为不稳定的。这就导致Compose可能会频繁地对这些类进行重新组合,以防它们的状态可能已经改变。然而,通过使用这两个特定的注解,我们可以防止这种不必要的重新组合。
Stable
@Stable注解是一个承诺,向Compose编译器表明这个对象可能会发生变化。但每当对象发生变化时,我们会保证Compose运行时都会得到通知。因此,Compose运行时可以将其视为稳定的类,从而避免非必要的重组。让我们来看一个例子:
@Stable
class ProductListState(
val products: List<Product>,
val isLoading: Boolean
)
//usage
@Composable
fun ShowProducts(
modifier: Modifier = Modifier,
productListState: ProductListState
) {
LazyColumn(modifier = modifier) {
items(productListState.products) {
Text(text = it.name)
}
}
}
Immutable
@Immutable用于标记完全不可变的类。一旦创建了此类的一个对象,其中的任何内容都不能在这个对象中改变,从而减少了不必要的重新组合。
注意:如果您向列表中添加或删除一个项目,由于您使用了“@Immutable”,组合件将永远不会被重新创建。
@Immutable
class ProductListState(
val products: List<Product>,
)
@Composable
fun ShowProducts(
modifier: Modifier = Modifier,
productListState: ProductListState
) {
LazyColumn(modifier = modifier) {
items(productListState.products) {
Text(text = it.name)
}
}
}
在上面这个例子中,我们使用Immutable来标记ProductListState类,这样即使它包含了List类型的参数,它也不会被编译器视为稳定类,从而在重组中跳过。
Key关键字
第二个就是需要为每个列表项提供Key作为唯一指引。这样在数据发生改动时,Compose可以自动保留未改变的列表项,减少更新范围。
我们先看一个没有使用Key的例子:
@Composable
fun NotesList(notes: List<Note>) {
LazyColumn {
items(
items = notes
) { note ->
NoteRow(note)
}
}
}
上面这个示例在正常情况下并没有什么问题,列表可以正常展示。但是如果我们对数据列表做出一些改动,将最后一个item移至第一个。在这种情况下,其实数据列表的内容并没有改变,只是顺序有一个调整。但是对于Compose编译器来说,它并没有办法识别到这个情况。它只能感知到每个列表项的数据都发生了改变,所以它会触发全量重组。这就会导致性能问题,因为实际上只有一个列表项发生了改变。
正确的做法应该是为每个列表项指定一个Key作为唯一标识,这样Compose编译器就能区分是数据改变还是仅仅是数据位置发生了改动:
@Composable
fun NotesList(notes: List<Note>) {
LazyColumn {
items(
items = notes,
key = { note ->
// 需要提供一个唯一标识
note.id
}
) { note ->
NoteRow(note)
}
}
}
副作用
第三点就是避免非必要的副作用。有时我们需要在组合式内部操作状态,但是当组合式修改状态时,就会发生不必要的副作用,这可能导致不可预测的行为和意外的重新组合。让我们来看一个例子:
@Composable
fun ExempleSideEffect() {
val state = remember { mutableStateOf(0) }
// Side effect: manage state inside composable
Button(onClick = { state.value++ }) {
Text("Click here")
}
//another Side effect
LaunchedEffect(key1 = state.value) {
Log.d("Exemple", "State changes to ${state.value}")
}
}
上面是一个错误示例,我们在Composable中使用LaunchedEffect启动了一个副作用,它以state.value作为Key。然而,我们又添加了一个button,它在被点击时会改变state.value。这样就会导致每次按钮点击时都会导致整个Composable重组,影响了性能。
正确的做法应该如下:
@Composable
fun ExempleCorrectSideEffect(onClickIncrement: () -> Unit, count: Int) {
// callback to handle the click button
Button(onClick = onClickIncrement) {
Text("Click here")
}
// shows the counter value
Text("Count: $count")
}
可以看到,我们将状态提升到了上层,使该Composable成为了无状态方法。并且,我们去除了多余的副作用。这样,我们就避免了全量重组的问题。
图片加载优化
最后一个就是图片加载的优化。我们可以通过以下几点来提升图片的加载性能及用户体验:
-
只有在需要时才加载图片,减少内存使用并提高应用程序的加载时间。
-
通过避免加载不在屏幕上的图片来节省资源。
-
与第三方的库(如Coil和Glide)集成,实现高效的图片管理。
让我们来看一个例子:
@Composable
fun LoadImageWithouLazyLoading(url: String) {
val bitmap = loadImageBitmap(url) // 耗时函数
Image(
bitmap = bitmap,
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
}
可以看到,上面的例子中使用了一个耗时函数来加载图片。这样的写法会导致每次重组时都调用耗时函数,获取到图片内容后再展示,这就会大大影响性能和用户体验。正确的做法应该如下:
@Composable
fun LazyLoadingWithCoil(url: String) {
Image(
painter = rememberImagePainter(
data = url,
builder = {
crossfade(true)
}
),
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
}
这里,我们使用了Coil库提供的rememberImagePainter来加载图片。这个方法会缓存图片的加载结果,这样在后续的重组中就可以直接复用,无需重新加载,大大提高了性能。
总结
在这篇文章中,我们探讨了几种优化Jetpack Compose性能的关键策略。通过在开发过程中采纳良好的实践,你可以显著提高Android应用程序的效率和响应性。希望它们对您有所帮助,谢谢!