声明式编程范式
长期以来,Android视图层次结构一直能够表示为界面widget树。由于运用的状况会因用户交互等要素而产生变化,因此界面层次结构需求进行更新以显现当前数据。最常见的界面更新办法是运用findViewById()
等函数遍历树,并经过调用button.setText(String)
、container.addChild(View)
或许img.setImageBitmap(Bitmap)
等办法更改节点。这些办法会改动widget的内部状况。
手动操纵视图会进步出错的或许性。假如一条数据在多个位置出现,很简略忘记更新显现它的某个视图。此外,当两项更新以出人意料的办法产生冲突时,也很简略形成异常状况。例如,某项更新或许会尝试设置从界面中移除的节点的值。一般来说,软件保护的复杂性会跟着需求更新的视图数量而增加。
在曩昔的几年中,整个行业已开端转向声明性界面模型,该模型大大简化了与构建和更新界面相关的工程使命。该技能的作业原理是在概念从头开端从头生成整个屏幕,然后仅履行必要的更改。此办法可防止手动更新有状况视图层次结构的复杂性。Compose是一个声明性界面结构。
从头生成整个屏幕所面对的一个难题是,在时刻、核算才能和电池用量方面或许本钱昂扬。为了削减在这方面耗费的资源,Compose会智能地挑选在任何给定时刻需求从头制作界面的哪个部分。这会对你设计界面组件的办法有一定影响。
简略的可组合函数
运用Compose,您能够经过界说一组承受数据而宣布界面元素的可组合函数来构建界面。一个简略的示例是Greeting
widget,它承受String
并宣布一个显现问好音讯的Text
widget。
图1.一个简略的可组合函数,可运用所传入的数据在屏幕上出现文本widget。
关于次函数,有几点值得注意:
- 次函数带有
@Composable
注释。一切可组合函数都必须带有此注释;此注释可告知Compose编辑器:此函数旨在将数据转换为界面。 - 此函数承受数据。可组合函数能够承受一些参数,这些参数可让运用逻辑描绘界面。在本例中,咱们的widget承受一个
String
,因此它能够按名称问好用户。 - 此函数能够在界面中显现文本。为此,它会调用
Text()
可组合函数,该函数实际上会创立文本界面元素。可组合函数经过调用其他可组合函数来宣布界面层次结构。
声明性范式改动
在许多面向对象的命令式界面工具包中,您能够经过实例化widget树来初始化界面。您一般经过膨胀XML布局文件来完结此目的。每个widget都保护自己的内部状况,而且供给getter和setter办法,答应运用逻辑与widget进行交互。
在Composr 的声明性办法中,widget相对无状况,而且不供给setter或getter函数。实际上,widget不会以形象方式供给。您能够经过调用带有不同参数的同一可组合函数来更新界面。这使得向架构模式(如ViewModel
)供给状况变得很简略。然后,可组合项负责在每次可调查数据更新时将当前运用状况转换为界面。
上图 运用逻辑为尖端可组合函数供给数据。该函数经过调用其他可组合函数来运用这些数据描绘界面,将恰当的数据给这些可组合函数,并沿层级结构向下传递数据。
当用户与界面交互时,界面会发起onClick
等事情。这些事情应通知运用逻辑,运用逻辑随后能够改动运用的状况。当状况产生变化时,系统会运用新数据再次调用可组合函数。这会导致从头制作界面元素,此进程称为“重组”。
用户与界面元素进行了交互,导致触发一个事情。运用逻辑响应该事情,然后系统根据需求运用新参数自动再次调用可组合函数。
重组
在命令式界面模型中,假如更改某个widget,您能够在该widget上调用setter以更改其内部状况。在Compose中,你能够运用更新数据再次调用组合函数。这样做会导致函数进行重组——系统会根据需求运用新数据从头制作函数宣布的widget。Compose结构能够智能地仅重组已更改的组件。
例如,假定有一下可组合函数,它用于显现一个按钮
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
Button(onClick = onClick) {
Text("I'va been clicked $clicks times")
}
}
每次点击该按钮时,调用方都会更新clicks
的值。Compose会再次调用lambda与Text
函数以实际新值;此进程称为“重组”。不依赖于该值的其他函数不会进行重组。
如前文所属,重组整个界面在核算本钱昂扬,由于会消耗核算才能并缩短电池续航时刻。Compose运用智能重组来处理此问题。
重组是指在输入更改时再次调用可组合函数的进程。当函数的输入更改时,会产生这种状况。当Composr根据新输入重组时,它仅调用或许已改的函数或lambda,而越过其余函数或lambda。经过越过一切未更改的函数或lambda,Compose能够高效得重组。
切勿依赖于履行可组合函数所产生的顺便效应,由于你或许会越过函数的重组。假如你这样做,用户或许会在您的运用中遇到奇怪且不可猜测的行为。顺便效应是指对运用的其余部分可见的任何更改。例如,以下操作全部都是风险的顺便效应:
- 写入同享对象的特点
- 更新
ViewModel
中的课调查项 - 更新同享偏好设置
可组合函数或许会像每一帧一样频频地从头履行,例如在出现动画时。可组合函数影快速履行,以防止在播映动画期间出现卡顿。假如您需求履行本钱昂扬的操作(例如从同享偏好设置去读数据),请在后台协程中履行,并将值结果作为参数传递给可组合函数。
例如,以下代码会创立一个可组合一更新SharedPreferences
中的值。该组合项不应从同享偏好设置本社你读取或写入,所以此代码将读取和写入操作移至后台协程中的ViewModel
。运用逻辑会运用回调传递当前值以动身更新。
@Composable
fun SharedPrefsToogle(
text: String,
value: Boolean,
onValueChanged: (Boolean) -> Unit
) {
Row {
Text(text)
CheckBox(checked = value, onCheckedChange = onValueChanged)
}
}
上面讨论和您在Compose中变成时需求注意的事项:
- 可组合函数能够按任何次序履行
- 可组合函数能够并行履行
- 重组会越过尽或许多的可组合函数和lambda
- 可组合函数或许会像动画的每一帧一样十分频频地运转 下面几个部分将介绍怎么构建可组合函数以支撑重组。在每种状况下,最佳实践都是使可组合函数坚持快速、幂等且没有顺便效应。
可组合函数能够按任何次序履行
假如您看一下可组合函数的代码,或许会认为这些代码按其出现的次序运转。但其实未必是这样。假如某个可组合函数包含对其他组合函数的调用,这些函数能够按任何次序运转。Compose能够挑选辨认某些界面元素的优先级高于其他界面元素,因而首要制作这些元素。
例如,假定你有如下代码,用于在标签布局中制作三个屏幕:
@Composable
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
}
}
对StartScreen
、MiddlScreen
和EndScreen
的调用能够按任何次序进行。这意味着,举例来说,您不能让StartScreen()
设置某个全局变量(顺便效应)并让MiddleScreen()
运用这项更改。相反,其间每个函数都需求坚持独立。
可组合函数能够并行运转
Compose能够经过并行运转可组合函数来优化重组。这样依赖,Compose就能够运用多个中心,并以较低的优先级运转可组合函数(不在屏幕上)。
这种优化意味着,可组合函数或许会在后台线程池中履行。假如某个可组合函数对ViewModel
调用一个函数,则Compose
或许会同时从多个线程调用该函数。
为了缺号运用正常运转,一切可组合函数都不应有顺便效应,而应经过一直在界面线程上履行的onClick
等回调动身顺便效应,
调用蘑菇额可组合函数时,调用或许产生在与调用方不同的线程上。这意味着,应防止运用修改可组合lambda中的变量额度代码,既由于此类代码并非线程安全代码,又由于它是可组合lambda不答应的顺便效应。
以下示例展现了一个和组合项,它显现一个列表及其项数:
@Composable
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Column {
for (item in myList) {
Text("Item: $item")
}
}
Text("Count:${myList.size}")
}
}
此代码没有顺便效应,它将会输入列表转换为界面。此代码十分合适显现小列表。不过,假如函数写入局部变量,则这并非线程安全或正确的代码:
@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")
}
}
在本例中,每次重组时,都会修改items
。这能够是动画的每一帧,或是在列表更新时。但不管怎样,界面都会显现过错的项数。因此,Copose不支撑这样的写入操作;经过禁止此类写入操作,咱们答应结构更改线程以履行可组合lambda。
重组会跳进尽或许多的内容
假如界面的某些部分无效,Compose会极力只重组需求更新的部分。这意味着,它能够越过某些内容以从头运转单个按钮的可组合项,而不履行界面树中在其上面或下面的任何可组合项。
每个可组合函数和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 recopose 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 ->
// Then an item's [name] updates, the adapter for that item
// will recopose. This will not recopose when [header] changes
NamePicker (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会从重组中放弃界面树。假如任何顺便效应依赖于显现的界面,则即使取消了组合操作,也会运用该顺便效应。这或许会导致运用状况不一致。
保证一切可组合函数和lambda都幂等且没有顺便效应,以处理达观的重组。
可组合函数或许会十分频频地运转
在某些状况下,或许会针对界面动画的每一帧运转一个可组合函数。假如该函数履行本钱昂扬的操作(例如从设备存储空间读取数据),或许会导致界面卡顿。
例如,假如您的widget尝试读取设备设置,它或许会在一秒读取这些设置数百次,这会对运用的性能形成灾难性的影响。
假如您的可组合函数需求数据,它应为相应的数据界说参数。然后,您能够将本钱昂扬的作业移至组成操作线程之外的其他线程,并运用mutableStateOf
或ListData
将相应的数据传递给Compose。
翻译原文:Compose编程思维