文章目录
- 一、声明式编程
- 二、@Composable 函数
- 三、动态内容
- 四、重组
- 4.1 @Composable 函数应独立、可按任意顺序执行
- 4.2 @Composable 函数可并行
- 5.3 重组会跳过尽量多的内容
一、声明式编程
通常过程式范式中,Android 的视图表示为 widget 树,通过 inflate xml 文件来实现布局。每个 widget 都有自己内部的状态,提供 getter() 和 setter() 方法,允许 App 和 widget 交互。当状态变化而导致需更新 UI 时,常用 findViewById() 函数,通过 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。
手动操作 view 容易出错,同时更新同一个 view 会数据不一致。为了简化工作,业界都用声明式了,例如前端的 Vue、React 都是如此。我们只需用 kotlin 声明 UI,并且改变变量,剩下的都交给 Compose 库:当变量改变时,其会帮我们重绘依赖于此变量的 UI。
在 Compose 声明式范式中,widget 无状态,不提供 getter 和 setter 方法。实际上,widget 不是以对象形式提供,而是以 @Composable 函数提供,且该函数可接受不同的参数。这样只要状态改变,Compose 会自动监听到状态变化,并替我们更新UI。
数据流向是这样的:
首先,应用逻辑向 @Composable 函数传入数据,如果是嵌套的 @Composable 函数,就逐级传递到最底层,示例如下:
其次,当用户操作界面时,界面会发起 onClick() 等事件,这些事件会通知应用逻辑,应用逻辑随后更改数据。当数据变化时,Compose 会通知使用了这些数据的 @Composable 函数重绘,示例如下:
二、@Composable 函数
通过 @Composable 函数,我们可以声明 UI。
@Composable 函数有如下特点:
- 带有 @Composable 的函数,Compose 编译器会将其转换为 UI。
- @Composable 函数可接收参数,用参数来描述 UI。例如上例中的 Greeting() 函数中的 String 参数。
- @Composable 函数可以嵌套,来构造更复杂的 UI。例如 Row() 嵌套 Column() ,再嵌套 Text() 等。
- @Composable 函数没有返回值。
- @Composable 函数快速、幂等、没有副作用。
- 多次地、用同一参数,调用此函数,行为需相同。(如全局变量,或 ramdom() 函数)
- 函数应没有副作用,如不应修改属性,或不应修改全局变量。
三、动态内容
由于可组合函数是用 Kotlin 而不是 XML 编写的,因此它们可以像其他任何 Kotlin 代码一样动态。可以使用 if 语句来确定是否要显示特定的界面元素。可以使用 for。可以调用辅助函数。有底层语言的全部灵活性。
例如,假设您想要构建一个界面,用来问候一些用户:
@Composable
fun Greeting(names: List<String>) {
for (name in names) {
Text("Hello $name")
}
}
四、重组
重组是指:输入改变时,再次调用 @Composable 函数的过程。只需改变数据,Compose 会监听到数据变化并使函数重组:用新数据重绘。
例如,当下例的 clicks 变量变化时,因 Button 依赖此变量而会重组,代码如下:
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I've been clicked $clicks times")
}
}
切勿依赖于 @Composable 函数所产生的附带效应,防止发生奇怪、不可预测的行为。附带效应是指:对应用的其余部分可见的修改。例如,以下操作是危险的附带效应:
- 写入共享对象
- 更新 ViewModel 的可观察项
- 更新 SharedPreferences
@Composable 函数可能会像每帧一样重复执行,所以应简洁快速,复杂的逻辑尽量放在后台协程中,并将协程的结果传给 @Composable 函数。例如,如果您的 widget 尝试读取设备设置,它可能会在一秒内读取这些设置数百次,这会对应用的性能造成灾难性的影响。如果您的可组合函数需要数据,它应为相应的数据定义参数。然后,您可以将成本高昂的工作移至组成操作线程之外的其他线程,并使用 mutableStateOf 或 LiveData 将相应的数据传递给 Compose。
4.1 @Composable 函数应独立、可按任意顺序执行
例如下例中,对 StartScreen、MiddleScreen 和 EndScreen 的调用可按任意顺序执行,即我们不能让 A() 函数设置某全局变量(附带效应)并让 B() 函数利用这次修改。而应当保持各函数的独立。
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
4.2 @Composable 函数可并行
调用@Composable 函数时,可能会优化在后台线程池中执行。所以应避免修改 @Composable 函数的变量,因为应避免这些附带效应,避免线程不安全。
下例就是无附带效应的例子
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count: ${myList.size}")
}
}
但如果函数写入局部变量,就线程不安全了。例如下例中,每次调用都会修改 items 变量,会有附带作用,代码如下:
@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
var items = 0
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
items++ // Avoid! Side-effect of the column recomposing.
}
}
Text("Count: $items")
}
}
5.3 重组会跳过尽量多的内容
Compose 置灰重组需要更新的部分,跳过无需更新的部分,例如下例代码:
/**
* 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()
// 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) }))
}