Jetpack Compose 是一个用于 Android 的现代声明式 UI 工具包。通过提供一个声明式 API,Compose 使编写和维护您的应用 UI 变得更容易,该 API 允许您渲染应用 UI,而无需强制修改前端视图。此术语需要一些解释,但其含义对您的应用设计至关重要。
声明式编程范式
从历史上看,Android 视图层次结构可以表示为 UI 小部件的树。由于用户交互等原因,应用的状态发生变化,因此需要更新 UI 层次结构以显示当前数据。更新 UI 最常见的方法是使用诸如findViewById()
之类的函数遍历树,并通过调用诸如button.setText(String)
、container.addChild(View)
或img.setImageBitmap(Bitmap)
之类的方法更改节点。这些方法更改了小部件的内部状态。
手动操作视图会增加错误的可能性。如果某段数据在多个地方呈现,则很容易忘记更新显示该数据的一个视图。也很容易创建非法状态,当两个更新以意外的方式发生冲突时。例如,更新可能会尝试设置一个刚刚从 UI 中删除的节点的值。通常,软件维护的复杂性随着需要更新的视图数量而增加。
在过去的几年里,整个行业已经开始转向声明式 UI 模型,这大大简化了构建和更新用户界面的相关工程工作。该技术的工作原理是从概念上重新生成整个屏幕,然后仅应用必要的更改。这种方法避免了手动更新有状态视图层次结构的复杂性。Compose 是一个声明式 UI 框架。
重新生成整个屏幕的一个挑战是,它在时间、计算能力和电池使用方面可能代价高昂。为了减轻此成本,Compose 会智能地选择 UI 中哪些部分需要在任何给定时间重新绘制。这确实对您设计 UI 组件的方式有一些影响,如重组中所述。
一个简单的可组合函数
使用 Compose,您可以通过定义一组可组合函数来构建用户界面,这些函数接收数据并发出 UI 元素。一个简单的例子是Greeting
小部件,它接收一个String
并发出一个Text
小部件,用于显示问候消息。
图 1. 一个简单的可组合函数,它接收数据并使用它在屏幕上渲染文本小部件。
关于此函数的一些值得注意的事情
该函数用
@Composable
注解进行注释。所有可组合函数都必须具有此注解;此注解通知 Compose 编译器此函数旨在将数据转换为 UI。该函数接收数据。可组合函数可以接受参数,这允许应用逻辑描述 UI。在本例中,我们的部件接受一个
String
,以便它可以按姓名向用户问候。该函数在 UI 中显示文本。它通过调用
Text()
可组合函数来实现,该函数实际上创建了文本 UI 元素。可组合函数通过调用其他可组合函数来发出 UI 层次结构。该函数不返回任何内容。发出 UI 的 Compose 函数不需要返回任何内容,因为它们描述了所需的屏幕状态,而不是构建 UI 小部件。
此函数速度很快,幂等且没有副作用。
- 当使用相同的参数多次调用该函数时,它的行为方式相同,并且它不使用其他值,例如全局变量或对
random()
的调用。 - 该函数描述了 UI,没有任何副作用,例如修改属性或全局变量。
通常,所有可组合函数都应使用这些属性编写,原因在重组中进行了讨论。
- 当使用相同的参数多次调用该函数时,它的行为方式相同,并且它不使用其他值,例如全局变量或对
声明式范式的转变
对于许多命令式面向对象 UI 工具包,您通过实例化小部件树来初始化 UI。您通常通过膨胀 XML 布局文件来执行此操作。每个小部件都维护自己的内部状态,并公开 getter 和 setter 方法,允许应用逻辑与小部件交互。
在 Compose 的声明式方法中,小部件相对无状态,不公开 setter 或 getter 函数。事实上,小部件不会作为对象公开。您通过使用不同的参数调用相同的可组合函数来更新 UI。这使得向诸如ViewModel
之类的架构模式提供状态变得很容易,如应用架构指南中所述。然后,您的可组合函数负责在可观察数据更新的每次更新时将当前应用状态转换为 UI。
图 2. 应用逻辑向顶级可组合函数提供数据。该函数使用数据通过调用其他可组合函数来描述 UI,并将适当的数据传递给这些可组合函数,然后向下传递到层次结构中。
当用户与 UI 交互时,UI 会引发诸如onClick
之类的事件。这些事件应通知应用逻辑,然后应用逻辑可以更改应用的状态。当状态更改时,可组合函数会使用新数据再次被调用。这会导致 UI 元素重新绘制——此过程称为重组。
图 3. 用户与 UI 元素交互,导致触发事件。应用逻辑响应事件,然后可组合函数会自动再次使用新参数调用,如果需要的话。
动态内容
由于可组合函数是用 Kotlin 而不是 XML 编写的,因此它们可以像任何其他 Kotlin 代码一样动态。例如,假设您想构建一个向用户列表问候的 UI
@Composable fun Greeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
此函数接收一个名称列表,并为每个用户生成一个问候语。可组合函数可以非常复杂。您可以使用if
语句来决定是否要显示特定的 UI 元素。您可以使用循环。您可以调用辅助函数。您拥有底层语言的全部灵活性。这种强大和灵活是 Jetpack Compose 的主要优势之一。
重组
在命令式 UI 模型中,要更改小部件,您需要调用小部件上的 setter 以更改其内部状态。在 Compose 中,您需要再次使用新数据调用可组合函数。这样做会导致函数被重组——如果需要,函数发出的部件将使用新数据重新绘制。Compose 框架可以智能地仅重组已更改的组件。
例如,考虑此可组合函数,它显示一个按钮
@Composable fun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") } }
每次单击按钮时,调用方都会更新clicks
的值。Compose 再次使用Text
函数调用 lambda 以显示新值;此过程称为重组。不依赖于该值的其他函数不会被重组。
正如我们所讨论的,重组整个 UI 树在计算上可能代价高昂,这会消耗计算能力和电池电量。Compose 通过这种智能重组解决了这个问题。
重组是在输入发生变化时再次调用可组合函数的过程。当函数的输入发生变化时,就会发生这种情况。当 Compose 基于新输入进行重组时,它只会调用可能已更改的函数或 lambda,并跳过其余函数。通过跳过所有没有更改参数的函数或 lambda,Compose 可以高效地进行重组。
永远不要依赖于执行可组合函数的副作用,因为函数的重组可能会被跳过。如果您这样做,用户可能会在您的应用中遇到奇怪且不可预测的行为。副作用是指对应用其余部分可见的任何更改。例如,以下操作都是危险的副作用
- 写入共享对象的属性
- 更新
ViewModel
中的可观察对象 - 更新共享首选项
可组合函数可能会像渲染动画时那样频繁地重新执行,例如每帧执行一次。可组合函数应该速度很快,以避免在动画期间出现卡顿。如果您需要执行昂贵的操作,例如从共享首选项读取,请在后台协程中执行,并将值结果作为参数传递给可组合函数。
例如,此代码创建了一个可组合函数来更新SharedPreferences
中的值。可组合函数本身不应从共享首选项读取或写入。相反,此代码将读取和写入操作移动到后台协程中的ViewModel
中。应用逻辑使用回调传递当前值以触发更新。
@Composable fun SharedPrefsToggle( text: String, value: Boolean, onValueChanged: (Boolean) -> Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
本文档讨论了使用 Compose 时需要注意的一些事项
- 重新组合尽可能跳过可组合函数和 lambda 表达式。
- 重新组合是乐观的,并且可能会被取消。
- 可组合函数可能会非常频繁地运行,甚至可以像动画的每一帧一样频繁。
- 可组合函数可以并行执行。
- 可组合函数可以以任何顺序执行。
以下部分将介绍如何构建可组合函数以支持重新组合。在每种情况下,最佳实践都是保持可组合函数快速、幂等且无副作用。
重新组合尽可能跳过
当 UI 的部分无效时,Compose 会尽最大努力仅重新组合需要更新的部分。这意味着它可能会跳过重新运行单个 Button 的可组合函数,而无需执行 UI 树中其上方或下方的任何可组合函数。
每个可组合函数和 lambda 表达式都可能会自行重新组合。以下是一个示例,演示了在渲染列表时重新组合如何跳过某些元素
/** * 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.bodyLarge) HorizontalDivider() // LazyColumn is the Compose version of a RecyclerView. // The lambda passed to items() is similar to a RecyclerView.ViewHolder. LazyColumn { items(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) })) }
这些作用域中的每一个都可能是重新组合期间唯一执行的内容。当 header
发生更改时,Compose 可能会跳过到 Column
lambda 表达式,而无需执行其任何父级。并且在执行 Column
时,如果 names
没有更改,Compose 可能会选择跳过 LazyColumn
的项。
同样,执行所有可组合函数或 lambda 表达式都应无副作用。当您需要执行副作用时,请从回调中触发它。
重新组合是乐观的
只要 Compose 认为可组合函数的参数可能已更改,就会开始重新组合。重新组合是乐观的,这意味着 Compose 预计在参数再次更改之前完成重新组合。如果参数在重新组合完成之前确实发生了更改,Compose 可能会取消重新组合并使用新参数重新启动它。
当重新组合被取消时,Compose 会丢弃来自重新组合的 UI 树。如果您有任何依赖于 UI 显示的副作用,即使组合被取消,该副作用也会被应用。这可能导致应用程序状态不一致。
确保所有可组合函数和 lambda 表达式都是幂等且无副作用的,以处理乐观的重新组合。
可组合函数可能会非常频繁地运行
在某些情况下,可组合函数可能会针对 UI 动画的每一帧运行。如果该函数执行代价高昂的操作(例如从设备存储读取),则该函数可能会导致 UI 卡顿。
例如,如果您的 widget 尝试读取设备设置,它可能会每秒读取这些设置数百次,这对应用程序的性能产生灾难性的影响。
如果您的可组合函数需要数据,则应为数据定义参数。然后,您可以将代价高昂的工作移动到另一个线程(在组合之外),并使用 mutableStateOf
或 LiveData
将数据传递给 Compose。
可组合函数可以在并行运行
Compose 可以通过并行运行可组合函数来优化重新组合。这将使 Compose 能够利用多个内核,并以较低的优先级运行屏幕上未显示的可组合函数。
此优化意味着可组合函数可能会在一个后台线程池中执行。如果可组合函数调用 ViewModel
上的函数,Compose 可能会同时从多个线程调用该函数。
为了确保您的应用程序行为正确,所有可组合函数都应无副作用。相反,请从回调(如 onClick
)中触发副作用,这些回调始终在 UI 线程上执行。
当调用可组合函数时,调用可能发生在与调用方不同的线程上。这意味着应避免修改可组合 lambda 表达式中变量的代码——这既是因为此类代码不是线程安全的,也是因为它是不允许的可组合 lambda 表达式的副作用。
以下是一个示例,显示一个显示列表及其计数的可组合函数
@Composable fun ListComposable(myList: List<String>) { Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") } }
此代码无副作用,并将输入列表转换为 UI。这是显示小型列表的优秀代码。但是,如果该函数写入局部变量,则此代码将不是线程安全或正确的
@Composable fun ListWithBug(myList: List<String>) { var items = 0 Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Card { Text("Item: $item") items++ // Avoid! Side-effect of the column recomposing. } } } Text("Count: $items") } }
在此示例中,items
在每次重新组合时都会被修改。这可能是动画的每一帧,也可能是列表更新时。无论哪种方式,UI 都将显示错误的计数。因此,Compose 中不支持这样的写入;通过禁止这些写入,我们允许框架更改线程以执行可组合 lambda 表达式。
可组合函数可以以任何顺序执行
如果您查看可组合函数的代码,您可能会假设代码按其出现的顺序运行。但这并非绝对保证。如果可组合函数包含对其他可组合函数的调用,则这些函数可能会以任何顺序运行。Compose 可以选择识别某些 UI 元素比其他元素优先级更高,并优先绘制它们。
例如,假设您有如下代码在选项卡布局中绘制三个屏幕
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
对 StartScreen
、MiddleScreen
和 EndScreen
的调用可能会以任何顺序发生。这意味着您不能,例如,让 StartScreen()
设置一些全局变量(副作用),并让 MiddleScreen()
利用该更改。相反,这些函数中的每一个都需要是自包含的。
了解更多
要了解有关如何在 Compose 和可组合函数中进行思考的更多信息,请查看以下其他资源。
视频
为您推荐
- 注意:当 JavaScript 关闭时显示链接文本
- Kotlin for Jetpack Compose
- 状态和 Jetpack Compose
- Jetpack Compose 架构分层